feat: show trash for soft deleted transcripts and hard delete option (#942)

* feat: show trash for soft deleted transcripts and hard delete option

* fix: test fixtures

* docs: aws new permissions
This commit is contained in:
Juan Diego García
2026-03-31 13:15:52 -05:00
committed by GitHub
parent cc9c5cd4a5
commit ec8b49738e
20 changed files with 1351 additions and 94 deletions

View File

@@ -34,11 +34,11 @@ export default function DeleteTranscriptDialog({
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header fontSize="lg" fontWeight="bold">
Delete transcript
Move to Trash
</Dialog.Header>
<Dialog.Body>
Are you sure you want to delete this transcript? This action cannot
be undone.
This transcript will be moved to the trash. You can restore it later
from the Trash view.
{title && (
<Text mt={3} fontWeight="600">
{title}
@@ -71,7 +71,7 @@ export default function DeleteTranscriptDialog({
ml={3}
disabled={!!isLoading}
>
Delete
Move to Trash
</Button>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -0,0 +1,83 @@
import React from "react";
import { Button, Dialog, Text } from "@chakra-ui/react";
interface DestroyTranscriptDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
cancelRef: React.RefObject<any>;
isLoading?: boolean;
title?: string;
date?: string;
source?: string;
}
export default function DestroyTranscriptDialog({
isOpen,
onClose,
onConfirm,
cancelRef,
isLoading,
title,
date,
source,
}: DestroyTranscriptDialogProps) {
return (
<Dialog.Root
open={isOpen}
onOpenChange={(e) => {
if (!e.open) onClose();
}}
initialFocusEl={() => cancelRef.current}
>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header fontSize="lg" fontWeight="bold">
Permanently Destroy Transcript
</Dialog.Header>
<Dialog.Body>
<Text color="red.600" fontWeight="medium">
This will permanently delete this transcript and all its
associated audio files. This action cannot be undone.
</Text>
{title && (
<Text mt={3} fontWeight="600">
{title}
</Text>
)}
{date && (
<Text color="gray.600" fontSize="sm">
Date: {date}
</Text>
)}
{source && (
<Text color="gray.600" fontSize="sm">
Source: {source}
</Text>
)}
</Dialog.Body>
<Dialog.Footer>
<Button
ref={cancelRef as any}
onClick={onClose}
disabled={!!isLoading}
variant="outline"
colorPalette="gray"
>
Cancel
</Button>
<Button
colorPalette="red"
onClick={onConfirm}
ml={3}
disabled={!!isLoading}
>
Destroy
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
);
}

View File

@@ -1,8 +1,9 @@
"use client";
import React from "react";
import { Box, Stack, Link, Heading } from "@chakra-ui/react";
import { Box, Stack, Link, Heading, Flex } from "@chakra-ui/react";
import NextLink from "next/link";
import { LuTrash2 } from "react-icons/lu";
import type { components } from "../../../reflector-api";
type Room = components["schemas"]["Room"];
@@ -13,6 +14,9 @@ interface FilterSidebarProps {
selectedSourceKind: SourceKind | null;
selectedRoomId: string;
onFilterChange: (sourceKind: SourceKind | null, roomId: string) => void;
isTrashView: boolean;
onTrashClick: () => void;
isAuthenticated: boolean;
}
export default function FilterSidebar({
@@ -20,6 +24,9 @@ export default function FilterSidebar({
selectedSourceKind,
selectedRoomId,
onFilterChange,
isTrashView,
onTrashClick,
isAuthenticated,
}: FilterSidebarProps) {
const myRooms = rooms.filter((room) => !room.is_shared);
const sharedRooms = rooms.filter((room) => room.is_shared);
@@ -32,8 +39,14 @@ export default function FilterSidebar({
fontSize="sm"
href="#"
onClick={() => onFilterChange(null, "")}
color={selectedSourceKind === null ? "blue.500" : "gray.600"}
fontWeight={selectedSourceKind === null ? "bold" : "normal"}
color={
!isTrashView && selectedSourceKind === null
? "blue.500"
: "gray.600"
}
fontWeight={
!isTrashView && selectedSourceKind === null ? "bold" : "normal"
}
>
All Transcripts
</Link>
@@ -51,12 +64,16 @@ export default function FilterSidebar({
href="#"
onClick={() => onFilterChange("room", room.id)}
color={
selectedSourceKind === "room" && selectedRoomId === room.id
!isTrashView &&
selectedSourceKind === "room" &&
selectedRoomId === room.id
? "blue.500"
: "gray.600"
}
fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id
!isTrashView &&
selectedSourceKind === "room" &&
selectedRoomId === room.id
? "bold"
: "normal"
}
@@ -79,12 +96,16 @@ export default function FilterSidebar({
href="#"
onClick={() => onFilterChange("room" as SourceKind, room.id)}
color={
selectedSourceKind === "room" && selectedRoomId === room.id
!isTrashView &&
selectedSourceKind === "room" &&
selectedRoomId === room.id
? "blue.500"
: "gray.600"
}
fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id
!isTrashView &&
selectedSourceKind === "room" &&
selectedRoomId === room.id
? "bold"
: "normal"
}
@@ -101,9 +122,15 @@ export default function FilterSidebar({
as={NextLink}
href="#"
onClick={() => onFilterChange("live", "")}
color={selectedSourceKind === "live" ? "blue.500" : "gray.600"}
color={
!isTrashView && selectedSourceKind === "live"
? "blue.500"
: "gray.600"
}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "live" ? "bold" : "normal"}
fontWeight={
!isTrashView && selectedSourceKind === "live" ? "bold" : "normal"
}
fontSize="sm"
>
Live Transcripts
@@ -112,13 +139,39 @@ export default function FilterSidebar({
as={NextLink}
href="#"
onClick={() => onFilterChange("file", "")}
color={selectedSourceKind === "file" ? "blue.500" : "gray.600"}
color={
!isTrashView && selectedSourceKind === "file"
? "blue.500"
: "gray.600"
}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "file" ? "bold" : "normal"}
fontWeight={
!isTrashView && selectedSourceKind === "file" ? "bold" : "normal"
}
fontSize="sm"
>
Uploaded Files
</Link>
{isAuthenticated && (
<>
<Box borderBottomWidth="1px" my={2} />
<Link
as={NextLink}
href="#"
onClick={onTrashClick}
color={isTrashView ? "red.600" : "red.500"}
_hover={{ color: "red.400" }}
fontWeight={isTrashView ? "bold" : "normal"}
fontSize="sm"
>
<Flex align="center" gap={1}>
<LuTrash2 />
Trash
</Flex>
</Link>
</>
)}
</Stack>
</Box>
);

View File

@@ -1,17 +1,21 @@
import React from "react";
import { IconButton, Icon, Menu } from "@chakra-ui/react";
import { LuMenu, LuTrash, LuRotateCw } from "react-icons/lu";
import { IconButton, Menu } from "@chakra-ui/react";
import { LuMenu, LuTrash, LuRotateCw, LuUndo2 } from "react-icons/lu";
interface TranscriptActionsMenuProps {
transcriptId: string;
onDelete: (transcriptId: string) => void;
onReprocess: (transcriptId: string) => void;
onDelete?: (transcriptId: string) => void;
onReprocess?: (transcriptId: string) => void;
onRestore?: (transcriptId: string) => void;
onDestroy?: (transcriptId: string) => void;
}
export default function TranscriptActionsMenu({
transcriptId,
onDelete,
onReprocess,
onRestore,
onDestroy,
}: TranscriptActionsMenuProps) {
return (
<Menu.Root closeOnSelect={true} lazyMount={true}>
@@ -22,21 +26,42 @@ export default function TranscriptActionsMenu({
</Menu.Trigger>
<Menu.Positioner>
<Menu.Content>
<Menu.Item
value="reprocess"
onClick={() => onReprocess(transcriptId)}
>
<LuRotateCw /> Reprocess
</Menu.Item>
<Menu.Item
value="delete"
onClick={(e) => {
e.stopPropagation();
onDelete(transcriptId);
}}
>
<LuTrash /> Delete
</Menu.Item>
{onReprocess && (
<Menu.Item
value="reprocess"
onClick={() => onReprocess(transcriptId)}
>
<LuRotateCw /> Reprocess
</Menu.Item>
)}
{onDelete && (
<Menu.Item
value="delete"
onClick={(e) => {
e.stopPropagation();
onDelete(transcriptId);
}}
>
<LuTrash /> Delete
</Menu.Item>
)}
{onRestore && (
<Menu.Item value="restore" onClick={() => onRestore(transcriptId)}>
<LuUndo2 /> Restore
</Menu.Item>
)}
{onDestroy && (
<Menu.Item
value="destroy"
color="red.500"
onClick={(e) => {
e.stopPropagation();
onDestroy(transcriptId);
}}
>
<LuTrash /> Destroy
</Menu.Item>
)}
</Menu.Content>
</Menu.Positioner>
</Menu.Root>

View File

@@ -29,8 +29,11 @@ interface TranscriptCardsProps {
results: SearchResult[];
query: string;
isLoading?: boolean;
onDelete: (transcriptId: string) => void;
onReprocess: (transcriptId: string) => void;
isTrash?: boolean;
onDelete?: (transcriptId: string) => void;
onReprocess?: (transcriptId: string) => void;
onRestore?: (transcriptId: string) => void;
onDestroy?: (transcriptId: string) => void;
}
function highlightText(text: string, query: string): React.ReactNode {
@@ -102,13 +105,19 @@ const transcriptHref = (
function TranscriptCard({
result,
query,
isTrash,
onDelete,
onReprocess,
onRestore,
onDestroy,
}: {
result: SearchResult;
query: string;
onDelete: (transcriptId: string) => void;
onReprocess: (transcriptId: string) => void;
isTrash?: boolean;
onDelete?: (transcriptId: string) => void;
onReprocess?: (transcriptId: string) => void;
onRestore?: (transcriptId: string) => void;
onDestroy?: (transcriptId: string) => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
@@ -136,22 +145,36 @@ function TranscriptCard({
};
return (
<Box borderWidth={1} p={4} borderRadius="md" fontSize="sm">
<Box
borderWidth={1}
p={4}
borderRadius="md"
fontSize="sm"
borderLeftWidth={isTrash ? "3px" : 1}
borderLeftColor={isTrash ? "red.400" : undefined}
bg={isTrash ? "gray.50" : undefined}
>
<Flex justify="space-between" alignItems="flex-start" gap="2">
<Box>
<TranscriptStatusIcon status={result.status} />
</Box>
<Box flex="1">
{/* Title with highlighting and text fragment for deep linking */}
<Link
as={NextLink}
href={transcriptHref(result.id, mainSnippet, query)}
fontWeight="600"
display="block"
mb={2}
>
{highlightText(resultTitle, query)}
</Link>
{/* Title — plain text in trash (deleted transcripts return 404) */}
{isTrash ? (
<Text fontWeight="600" mb={2} color="gray.600">
{highlightText(resultTitle, query)}
</Text>
) : (
<Link
as={NextLink}
href={transcriptHref(result.id, mainSnippet, query)}
fontWeight="600"
display="block"
mb={2}
>
{highlightText(resultTitle, query)}
</Link>
)}
{/* Metadata - Horizontal on desktop, vertical on mobile */}
<Flex
@@ -272,8 +295,10 @@ function TranscriptCard({
</Box>
<TranscriptActionsMenu
transcriptId={result.id}
onDelete={onDelete}
onReprocess={onReprocess}
onDelete={isTrash ? undefined : onDelete}
onReprocess={isTrash ? undefined : onReprocess}
onRestore={isTrash ? onRestore : undefined}
onDestroy={isTrash ? onDestroy : undefined}
/>
</Flex>
</Box>
@@ -284,8 +309,11 @@ export default function TranscriptCards({
results,
query,
isLoading,
isTrash,
onDelete,
onReprocess,
onRestore,
onDestroy,
}: TranscriptCardsProps) {
return (
<Box position="relative">
@@ -315,8 +343,11 @@ export default function TranscriptCards({
key={result.id}
result={result}
query={query}
isTrash={isTrash}
onDelete={onDelete}
onReprocess={onReprocess}
onRestore={onRestore}
onDestroy={onDestroy}
/>
))}
</Stack>

View File

@@ -19,6 +19,7 @@ import {
parseAsStringLiteral,
} from "nuqs";
import { LuX } from "react-icons/lu";
import { toaster } from "../../components/ui/toaster";
import type { components } from "../../reflector-api";
type Room = components["schemas"]["Room"];
@@ -29,6 +30,9 @@ import {
useTranscriptsSearch,
useTranscriptDelete,
useTranscriptProcess,
useTranscriptRestore,
useTranscriptDestroy,
useAuthReady,
} from "../../lib/apiHooks";
import FilterSidebar from "./_components/FilterSidebar";
import Pagination, {
@@ -40,6 +44,7 @@ import Pagination, {
} from "./_components/Pagination";
import TranscriptCards from "./_components/TranscriptCards";
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
import DestroyTranscriptDialog from "./_components/DestroyTranscriptDialog";
import { formatLocalDate } from "../../lib/time";
import { RECORD_A_MEETING_URL } from "../../api/urls";
import { useUserName } from "../../lib/useUserName";
@@ -175,14 +180,17 @@ const UnderSearchFormFilterIndicators: React.FC<{
const EmptyResult: React.FC<{
searchQuery: string;
}> = ({ searchQuery }) => {
isTrash?: boolean;
}> = ({ searchQuery, isTrash }) => {
return (
<Flex flexDir="column" alignItems="center" justifyContent="center" py={8}>
<Text textAlign="center">
{searchQuery
? `No results found for "${searchQuery}". Try adjusting your search terms.`
: "No transcripts found, but you can "}
{!searchQuery && (
{isTrash
? "Trash is empty."
: searchQuery
? `No results found for "${searchQuery}". Try adjusting your search terms.`
: "No transcripts found, but you can "}
{!isTrash && !searchQuery && (
<>
<Link href={RECORD_A_MEETING_URL} color="blue.500">
record a meeting
@@ -196,6 +204,8 @@ const EmptyResult: React.FC<{
};
export default function TranscriptBrowser() {
const { isAuthenticated } = useAuthReady();
const [urlSearchQuery, setUrlSearchQuery] = useQueryState(
"q",
parseAsString.withDefault("").withOptions({ shallow: false }),
@@ -216,6 +226,12 @@ export default function TranscriptBrowser() {
parseAsString.withDefault("").withOptions({ shallow: false }),
);
const [urlTrash, setUrlTrash] = useQueryState(
"trash",
parseAsStringLiteral(["1"] as const).withOptions({ shallow: false }),
);
const isTrashView = urlTrash === "1";
const [urlPage, setPage] = useQueryState(
"page",
parseAsInteger.withDefault(1).withOptions({ shallow: false }),
@@ -231,7 +247,7 @@ export default function TranscriptBrowser() {
return;
}
_setSafePage(maybePage.value);
}, [urlPage]);
}, [urlPage, setPage]);
const pageSize = 20;
@@ -240,11 +256,12 @@ export default function TranscriptBrowser() {
() => ({
q: urlSearchQuery,
extras: {
room_id: urlRoomId || undefined,
source_kind: urlSourceKind || undefined,
room_id: isTrashView ? undefined : urlRoomId || undefined,
source_kind: isTrashView ? undefined : urlSourceKind || undefined,
include_deleted: isTrashView ? true : undefined,
},
}),
[urlSearchQuery, urlRoomId, urlSourceKind],
[urlSearchQuery, urlRoomId, urlSourceKind, isTrashView],
);
const {
@@ -266,35 +283,55 @@ export default function TranscriptBrowser() {
const totalPages = getTotalPages(totalResults, pageSize);
// reset pagination when search results change (detected by total change; good enough approximation)
// reset pagination when search filters change
useEffect(() => {
// operation is idempotent
setPage(FIRST_PAGE).then(() => {});
}, [JSON.stringify(searchFilters)]);
}, [searchFilters, setPage]);
const userName = useUserName();
const [deletionLoading, setDeletionLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const cancelRef = React.useRef(null);
const destroyCancelRef = React.useRef(null);
// Delete (soft-delete / move to trash)
const [transcriptToDeleteId, setTranscriptToDeleteId] =
React.useState<string>();
// Destroy (hard-delete)
const [transcriptToDestroyId, setTranscriptToDestroyId] =
React.useState<string>();
const handleFilterTranscripts = (
sourceKind: SourceKind | null,
roomId: string,
) => {
if (isTrashView) {
setUrlTrash(null);
}
setUrlSourceKind(sourceKind);
setUrlRoomId(roomId);
setPage(1);
};
const handleTrashClick = () => {
setUrlTrash(isTrashView ? null : "1");
setUrlSourceKind(null);
setUrlRoomId(null);
setPage(1);
};
const onCloseDeletion = () => setTranscriptToDeleteId(undefined);
const onCloseDestroy = () => setTranscriptToDestroyId(undefined);
const deleteTranscript = useTranscriptDelete();
const processTranscript = useTranscriptProcess();
const restoreTranscript = useTranscriptRestore();
const destroyTranscript = useTranscriptDestroy();
const confirmDeleteTranscript = (transcriptId: string) => {
if (deletionLoading) return;
setDeletionLoading(true);
if (actionLoading) return;
setActionLoading(true);
deleteTranscript.mutate(
{
params: {
@@ -303,12 +340,12 @@ export default function TranscriptBrowser() {
},
{
onSuccess: () => {
setDeletionLoading(false);
setActionLoading(false);
onCloseDeletion();
reloadSearch();
},
onError: () => {
setDeletionLoading(false);
setActionLoading(false);
},
},
);
@@ -322,18 +359,83 @@ export default function TranscriptBrowser() {
});
};
const handleRestoreTranscript = (transcriptId: string) => {
if (actionLoading) return;
setActionLoading(true);
restoreTranscript.mutate(
{
params: {
path: { transcript_id: transcriptId },
},
},
{
onSuccess: () => {
setActionLoading(false);
reloadSearch();
toaster.create({
duration: 3000,
render: () => (
<Box bg="green.500" color="white" px={4} py={3} borderRadius="md">
<Text fontWeight="bold">Transcript restored</Text>
</Box>
),
});
},
onError: () => {
setActionLoading(false);
},
},
);
};
const confirmDestroyTranscript = (transcriptId: string) => {
if (actionLoading) return;
setActionLoading(true);
destroyTranscript.mutate(
{
params: {
path: { transcript_id: transcriptId },
},
},
{
onSuccess: () => {
setActionLoading(false);
onCloseDestroy();
reloadSearch();
},
onError: () => {
setActionLoading(false);
},
},
);
};
// Dialog data for delete
const transcriptToDelete = results?.find(
(i) => i.id === transcriptToDeleteId,
);
const dialogTitle = transcriptToDelete?.title || "Unnamed Transcript";
const dialogDate = transcriptToDelete?.created_at
const deleteDialogTitle = transcriptToDelete?.title || "Unnamed Transcript";
const deleteDialogDate = transcriptToDelete?.created_at
? formatLocalDate(transcriptToDelete.created_at)
: undefined;
const dialogSource =
const deleteDialogSource =
transcriptToDelete?.source_kind === "room" && transcriptToDelete?.room_id
? transcriptToDelete.room_name || transcriptToDelete.room_id
: transcriptToDelete?.source_kind;
// Dialog data for destroy
const transcriptToDestroy = results?.find(
(i) => i.id === transcriptToDestroyId,
);
const destroyDialogTitle = transcriptToDestroy?.title || "Unnamed Transcript";
const destroyDialogDate = transcriptToDestroy?.created_at
? formatLocalDate(transcriptToDestroy.created_at)
: undefined;
const destroyDialogSource =
transcriptToDestroy?.source_kind === "room" && transcriptToDestroy?.room_id
? transcriptToDestroy.room_name || transcriptToDestroy.room_id
: transcriptToDestroy?.source_kind;
if (searchLoading && results.length === 0) {
return (
<Flex
@@ -361,17 +463,24 @@ export default function TranscriptBrowser() {
mb={4}
>
<Heading size="lg">
{userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "}
{(searchLoading || deletionLoading) && <Spinner size="sm" />}
{isTrashView
? "Trash"
: userName
? `${userName}'s Transcriptions`
: "Your Transcriptions"}{" "}
{(searchLoading || actionLoading) && <Spinner size="sm" />}
</Heading>
</Flex>
<Flex flexDir={{ base: "column", md: "row" }}>
<FilterSidebar
rooms={rooms}
selectedSourceKind={urlSourceKind}
selectedRoomId={urlRoomId}
selectedSourceKind={isTrashView ? null : urlSourceKind}
selectedRoomId={isTrashView ? "" : urlRoomId}
onFilterChange={handleFilterTranscripts}
isTrashView={isTrashView}
onTrashClick={handleTrashClick}
isAuthenticated={isAuthenticated}
/>
<Flex
@@ -384,8 +493,8 @@ export default function TranscriptBrowser() {
>
<SearchForm
setPage={setPage}
sourceKind={urlSourceKind}
roomId={urlRoomId}
sourceKind={isTrashView ? null : urlSourceKind}
roomId={isTrashView ? null : urlRoomId}
searchQuery={urlSearchQuery}
setSearchQuery={setUrlSearchQuery}
setSourceKind={setUrlSourceKind}
@@ -406,12 +515,15 @@ export default function TranscriptBrowser() {
results={results}
query={urlSearchQuery}
isLoading={searchLoading}
onDelete={setTranscriptToDeleteId}
onReprocess={handleProcessTranscript}
isTrash={isTrashView}
onDelete={isTrashView ? undefined : setTranscriptToDeleteId}
onReprocess={isTrashView ? undefined : handleProcessTranscript}
onRestore={isTrashView ? handleRestoreTranscript : undefined}
onDestroy={isTrashView ? setTranscriptToDestroyId : undefined}
/>
{!searchLoading && results.length === 0 && (
<EmptyResult searchQuery={urlSearchQuery} />
<EmptyResult searchQuery={urlSearchQuery} isTrash={isTrashView} />
)}
</Flex>
</Flex>
@@ -423,10 +535,24 @@ export default function TranscriptBrowser() {
transcriptToDeleteId && confirmDeleteTranscript(transcriptToDeleteId)
}
cancelRef={cancelRef}
isLoading={deletionLoading}
title={dialogTitle}
date={dialogDate}
source={dialogSource}
isLoading={actionLoading}
title={deleteDialogTitle}
date={deleteDialogDate}
source={deleteDialogSource}
/>
<DestroyTranscriptDialog
isOpen={!!transcriptToDestroyId}
onClose={onCloseDestroy}
onConfirm={() =>
transcriptToDestroyId &&
confirmDestroyTranscript(transcriptToDestroyId)
}
cancelRef={destroyCancelRef}
isLoading={actionLoading}
title={destroyDialogTitle}
date={destroyDialogDate}
source={destroyDialogSource}
/>
</Flex>
);

View File

@@ -136,6 +136,7 @@ export function UserEventsProvider({
switch (msg.event) {
case "TRANSCRIPT_CREATED":
case "TRANSCRIPT_DELETED":
case "TRANSCRIPT_RESTORED":
case "TRANSCRIPT_STATUS":
case "TRANSCRIPT_FINAL_TITLE":
case "TRANSCRIPT_DURATION":

View File

@@ -57,6 +57,7 @@ export function useTranscriptsSearch(
offset?: number;
room_id?: string;
source_kind?: SourceKind;
include_deleted?: boolean;
} = {},
) {
return $api.useQuery(
@@ -70,6 +71,7 @@ export function useTranscriptsSearch(
offset: options.offset,
room_id: options.room_id,
source_kind: options.source_kind,
include_deleted: options.include_deleted,
},
},
},
@@ -105,6 +107,38 @@ export function useTranscriptProcess() {
});
}
export function useTranscriptRestore() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation("post", "/v1/transcripts/{transcript_id}/restore", {
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ["get", TRANSCRIPT_SEARCH_URL],
});
},
onError: (error) => {
setError(error as Error, "There was an error restoring the transcript");
},
});
}
export function useTranscriptDestroy() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation("delete", "/v1/transcripts/{transcript_id}/destroy", {
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ["get", TRANSCRIPT_SEARCH_URL],
});
},
onError: (error) => {
setError(error as Error, "There was an error destroying the transcript");
},
});
}
const ACTIVE_TRANSCRIPT_STATUSES = new Set<TranscriptStatus>([
"processing",
"uploaded",

View File

@@ -388,6 +388,46 @@ export interface paths {
patch: operations["v1_transcript_update"];
trace?: never;
};
"/v1/transcripts/{transcript_id}/restore": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Transcript Restore
* @description Restore a soft-deleted transcript.
*/
post: operations["v1_transcript_restore"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/transcripts/{transcript_id}/destroy": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Transcript Destroy
* @description Permanently delete a transcript and all associated files.
*/
delete: operations["v1_transcript_destroy"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/transcripts/{transcript_id}/topics": {
parameters: {
query?: never;
@@ -2391,6 +2431,14 @@ export interface components {
*/
title: string;
};
/** UserTranscriptRestoredData */
UserTranscriptRestoredData: {
/**
* Id
* @description A non-empty string
*/
id: string;
};
/** UserTranscriptStatusData */
UserTranscriptStatusData: {
/**
@@ -2446,6 +2494,15 @@ export interface components {
event: "TRANSCRIPT_FINAL_TITLE";
data: components["schemas"]["UserTranscriptFinalTitleData"];
};
/** UserWsTranscriptRestored */
UserWsTranscriptRestored: {
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
event: "TRANSCRIPT_RESTORED";
data: components["schemas"]["UserTranscriptRestoredData"];
};
/** UserWsTranscriptStatus */
UserWsTranscriptStatus: {
/**
@@ -3293,6 +3350,7 @@ export interface operations {
from?: string | null;
/** @description Filter transcripts created on or before this datetime (ISO 8601 with timezone) */
to?: string | null;
include_deleted?: boolean;
};
header?: never;
path?: never;
@@ -3427,6 +3485,68 @@ export interface operations {
};
};
};
v1_transcript_restore: {
parameters: {
query?: never;
header?: never;
path: {
transcript_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeletionStatus"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
v1_transcript_destroy: {
parameters: {
query?: never;
header?: never;
path: {
transcript_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeletionStatus"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
v1_transcript_get_topics: {
parameters: {
query?: never;
@@ -3995,9 +4115,7 @@ export interface operations {
};
v1_transcript_get_video_url: {
parameters: {
query?: {
token?: string | null;
};
query?: never;
header?: never;
path: {
transcript_id: string;
@@ -4254,6 +4372,7 @@ export interface operations {
"application/json":
| components["schemas"]["UserWsTranscriptCreated"]
| components["schemas"]["UserWsTranscriptDeleted"]
| components["schemas"]["UserWsTranscriptRestored"]
| components["schemas"]["UserWsTranscriptStatus"]
| components["schemas"]["UserWsTranscriptFinalTitle"]
| components["schemas"]["UserWsTranscriptDuration"];