mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
📂 refactor: Improve FileAttachment & File Form Deletion (#7471)
* refactor: optional attachment properties for `FileAttachment` * refactor: update ActionButton to use localized text * chore: localize text in DataTableFile, add missing translation, imports order, and linting * chore: linting in DataTable * fix: integrate Recoil state management for file deletion in DataTableFile * fix: integrate Recoil state management for file deletion in DataTable * fix: add temp_file_id to BatchFile type and update deleteFiles logic to properly remove files that are mapped to temp_file_id
This commit is contained in:
parent
e86842fd19
commit
eb1668ff22
7 changed files with 65 additions and 40 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ListFilter } from 'lucide-react';
|
import { ListFilter } from 'lucide-react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
|
@ -36,6 +37,7 @@ import { TrashIcon, Spinner } from '~/components/svg';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { useMediaQuery } from '~/hooks';
|
import { useMediaQuery } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
|
@ -60,12 +62,14 @@ type Style = {
|
||||||
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const setFiles = useSetRecoilState(store.filesByIndex(0));
|
||||||
|
const { deleteFiles } = useDeleteFilesFromTable(() => setIsDeleting(false));
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
const { deleteFiles } = useDeleteFilesFromTable(() => setIsDeleting(false));
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
|
@ -96,7 +100,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
const filesToDelete = table
|
const filesToDelete = table
|
||||||
.getFilteredSelectedRowModel()
|
.getFilteredSelectedRowModel()
|
||||||
.rows.map((row) => row.original);
|
.rows.map((row) => row.original);
|
||||||
deleteFiles({ files: filesToDelete as TFile[] });
|
deleteFiles({ files: filesToDelete as TFile[], setFiles });
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}}
|
}}
|
||||||
disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting}
|
disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting}
|
||||||
|
|
@ -218,13 +222,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
<div className="flex items-center justify-end gap-2 py-4">
|
<div className="flex items-center justify-end gap-2 py-4">
|
||||||
<div className="ml-2 flex-1 truncate text-xs text-muted-foreground sm:ml-4 sm:text-sm">
|
<div className="ml-2 flex-1 truncate text-xs text-muted-foreground sm:ml-4 sm:text-sm">
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{localize(
|
{localize('com_files_number_selected', {
|
||||||
'com_files_number_selected',
|
|
||||||
{
|
|
||||||
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||||
1: `${table.getFilteredRowModel().rows.length}`,
|
1: `${table.getFilteredRowModel().rows.length}`,
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="sm:hidden">
|
<span className="sm:hidden">
|
||||||
{`${table.getFilteredSelectedRowModel().rows.length}/${
|
{`${table.getFilteredSelectedRowModel().rows.length}/${
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import Image from '~/components/Chat/Messages/Content/Image';
|
||||||
import { useAttachmentLink } from './LogLink';
|
import { useAttachmentLink } from './LogLink';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
const FileAttachment = memo(({ attachment }: { attachment: Partial<TAttachment> }) => {
|
||||||
const { handleDownload } = useAttachmentLink({
|
const { handleDownload } = useAttachmentLink({
|
||||||
href: attachment.filepath,
|
href: attachment.filepath ?? '',
|
||||||
filename: attachment.filename,
|
filename: attachment.filename ?? '',
|
||||||
});
|
});
|
||||||
const extension = attachment.filename.split('.').pop();
|
const extension = attachment.filename?.split('.').pop();
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -19,6 +19,9 @@ const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!attachment.filepath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -88,6 +91,8 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
return <ImageAttachment attachment={attachment} />;
|
return <ImageAttachment attachment={attachment} />;
|
||||||
|
} else if (!attachment.filepath) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return <FileAttachment attachment={attachment} />;
|
return <FileAttachment attachment={attachment} />;
|
||||||
}
|
}
|
||||||
|
|
@ -119,9 +124,11 @@ export function AttachmentGroup({ attachments }: { attachments?: TAttachment[] }
|
||||||
<>
|
<>
|
||||||
{fileAttachments.length > 0 && (
|
{fileAttachments.length > 0 && (
|
||||||
<div className="my-2 flex flex-wrap items-center gap-2.5">
|
<div className="my-2 flex flex-wrap items-center gap-2.5">
|
||||||
{fileAttachments.map((attachment, index) => (
|
{fileAttachments.map((attachment, index) =>
|
||||||
|
attachment.filepath ? (
|
||||||
<FileAttachment attachment={attachment} key={`file-${index}`} />
|
<FileAttachment attachment={attachment} key={`file-${index}`} />
|
||||||
))}
|
) : null,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{imageAttachments.length > 0 && (
|
{imageAttachments.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CrossIcon } from '~/components/svg';
|
|
||||||
import { Button } from '~/components/ui';
|
import { Button } from '~/components/ui';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
type ActionButtonProps = {
|
type ActionButtonProps = {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ActionButton({ onClick }: ActionButtonProps) {
|
export default function ActionButton({ onClick }: ActionButtonProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
<div className="w-32">
|
<div className="w-32">
|
||||||
<Button
|
<Button
|
||||||
className="w-full rounded-md border border-black bg-white p-0 text-black hover:bg-black hover:text-white"
|
className="w-full rounded-md border border-black bg-white p-0 text-black hover:bg-black hover:text-white"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
Action Button
|
{/* Action Button */}
|
||||||
|
{localize('com_ui_action_button')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ListFilter } from 'lucide-react';
|
import { ListFilter } from 'lucide-react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
|
@ -18,24 +19,25 @@ import { FileContext } from 'librechat-data-provider';
|
||||||
import type { AugmentedColumnDef } from '~/common';
|
import type { AugmentedColumnDef } from '~/common';
|
||||||
import type { TFile } from 'librechat-data-provider';
|
import type { TFile } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Input,
|
Input,
|
||||||
Table,
|
Table,
|
||||||
|
Button,
|
||||||
|
TableRow,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
} from '~/components/ui';
|
} from '~/components/ui';
|
||||||
|
import ActionButton from '~/components/Files/ActionButton';
|
||||||
import { useDeleteFilesFromTable } from '~/hooks/Files';
|
import { useDeleteFilesFromTable } from '~/hooks/Files';
|
||||||
import { TrashIcon, Spinner } from '~/components/svg';
|
import { TrashIcon, Spinner } from '~/components/svg';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
|
||||||
import ActionButton from '../ActionButton';
|
|
||||||
import UploadFileButton from './UploadFileButton';
|
import UploadFileButton from './UploadFileButton';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
|
@ -57,12 +59,14 @@ export default function DataTableFile<TData, TValue>({
|
||||||
data,
|
data,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const setFiles = useSetRecoilState(store.filesByIndex(0));
|
||||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
const [isDeleting, setIsDeleting] = React.useState(false);
|
||||||
|
const { deleteFiles } = useDeleteFilesFromTable(() => setIsDeleting(false));
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
const { deleteFiles } = useDeleteFilesFromTable(() => setIsDeleting(false));
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
|
@ -87,7 +91,7 @@ export default function DataTableFile<TData, TValue>({
|
||||||
<>
|
<>
|
||||||
<div className="mt-2 flex flex-col items-start">
|
<div className="mt-2 flex flex-col items-start">
|
||||||
<h2 className="text-lg">
|
<h2 className="text-lg">
|
||||||
<strong>Files</strong>
|
<strong>{localize('com_ui_files')}</strong>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-3 flex w-full flex-col-reverse justify-between md:flex-row">
|
<div className="mt-3 flex w-full flex-col-reverse justify-between md:flex-row">
|
||||||
<div className="mt-3 flex w-full flex-row justify-center gap-x-3 md:m-0 md:justify-start">
|
<div className="mt-3 flex w-full flex-row justify-center gap-x-3 md:m-0 md:justify-start">
|
||||||
|
|
@ -103,7 +107,7 @@ export default function DataTableFile<TData, TValue>({
|
||||||
const filesToDelete = table
|
const filesToDelete = table
|
||||||
.getFilteredSelectedRowModel()
|
.getFilteredSelectedRowModel()
|
||||||
.rows.map((row) => row.original);
|
.rows.map((row) => row.original);
|
||||||
deleteFiles({ files: filesToDelete as TFile[] });
|
deleteFiles({ files: filesToDelete as TFile[], setFiles });
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}}
|
}}
|
||||||
className="ml-1 gap-2 dark:hover:bg-gray-850/25 sm:ml-0"
|
className="ml-1 gap-2 dark:hover:bg-gray-850/25 sm:ml-0"
|
||||||
|
|
@ -242,13 +246,11 @@ export default function DataTableFile<TData, TValue>({
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
|
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
|
||||||
<div className="text-muted-foreground ml-2 flex-1 text-sm">
|
<div className="ml-2 flex-1 text-sm text-muted-foreground">
|
||||||
{localize(
|
{localize('com_files_number_selected', {
|
||||||
'com_files_number_selected', {
|
|
||||||
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||||
1: `${table.getFilteredRowModel().rows.length}`,
|
1: `${table.getFilteredRowModel().rows.length}`,
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="dark:border-gray-500 dark:hover:bg-gray-600"
|
className="dark:border-gray-500 dark:hover:bg-gray-600"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ const useFileDeletion = ({
|
||||||
assistant_id?: string;
|
assistant_id?: string;
|
||||||
tool_resource?: EToolResources;
|
tool_resource?: EToolResources;
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_batch, setFileDeleteBatch] = useState<t.BatchFile[]>([]);
|
const [_batch, setFileDeleteBatch] = useState<t.BatchFile[]>([]);
|
||||||
const setFilesToDelete = useSetFilesToDelete();
|
const setFilesToDelete = useSetFilesToDelete();
|
||||||
|
|
||||||
|
|
@ -109,22 +108,33 @@ const useFileDeletion = ({
|
||||||
|
|
||||||
const deleteFiles = useCallback(
|
const deleteFiles = useCallback(
|
||||||
({ files, setFiles }: { files: ExtendedFile[] | t.TFile[]; setFiles?: FileMapSetter }) => {
|
({ files, setFiles }: { files: ExtendedFile[] | t.TFile[]; setFiles?: FileMapSetter }) => {
|
||||||
const batchFiles = files.map((_file) => {
|
const batchFiles: t.BatchFile[] = [];
|
||||||
const { file_id, embedded, filepath = '', source = FileSources.local } = _file;
|
for (const _file of files) {
|
||||||
|
const {
|
||||||
|
file_id,
|
||||||
|
embedded,
|
||||||
|
temp_file_id,
|
||||||
|
filepath = '',
|
||||||
|
source = FileSources.local,
|
||||||
|
} = _file;
|
||||||
|
|
||||||
return {
|
batchFiles.push({
|
||||||
source,
|
source,
|
||||||
file_id,
|
file_id,
|
||||||
filepath,
|
filepath,
|
||||||
embedded,
|
temp_file_id,
|
||||||
};
|
embedded: embedded ?? false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (setFiles) {
|
if (setFiles) {
|
||||||
setFiles((currentFiles) => {
|
setFiles((currentFiles) => {
|
||||||
const updatedFiles = new Map(currentFiles);
|
const updatedFiles = new Map(currentFiles);
|
||||||
batchFiles.forEach((file) => {
|
batchFiles.forEach((file) => {
|
||||||
updatedFiles.delete(file.file_id);
|
updatedFiles.delete(file.file_id);
|
||||||
|
if (file.temp_file_id) {
|
||||||
|
updatedFiles.delete(file.temp_file_id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const filesToUpdate = Object.fromEntries(updatedFiles);
|
const filesToUpdate = Object.fromEntries(updatedFiles);
|
||||||
setFilesToDelete(filesToUpdate);
|
setFilesToDelete(filesToUpdate);
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,7 @@
|
||||||
"com_ui_2fa_setup": "Setup 2FA",
|
"com_ui_2fa_setup": "Setup 2FA",
|
||||||
"com_ui_2fa_verified": "Successfully verified Two-Factor Authentication",
|
"com_ui_2fa_verified": "Successfully verified Two-Factor Authentication",
|
||||||
"com_ui_accept": "I accept",
|
"com_ui_accept": "I accept",
|
||||||
|
"com_ui_action_button": "Action Button",
|
||||||
"com_ui_add": "Add",
|
"com_ui_add": "Add",
|
||||||
"com_ui_add_model_preset": "Add a model or preset for an additional response",
|
"com_ui_add_model_preset": "Add a model or preset for an additional response",
|
||||||
"com_ui_add_multi_conversation": "Add multi-conversation",
|
"com_ui_add_multi_conversation": "Add multi-conversation",
|
||||||
|
|
@ -641,6 +642,7 @@
|
||||||
"com_ui_expand_chat": "Expand Chat",
|
"com_ui_expand_chat": "Expand Chat",
|
||||||
"com_ui_export_convo_modal": "Export Conversation Modal",
|
"com_ui_export_convo_modal": "Export Conversation Modal",
|
||||||
"com_ui_field_required": "This field is required",
|
"com_ui_field_required": "This field is required",
|
||||||
|
"com_ui_files": "Files",
|
||||||
"com_ui_filter_prompts": "Filter Prompts",
|
"com_ui_filter_prompts": "Filter Prompts",
|
||||||
"com_ui_filter_prompts_name": "Filter prompts by name",
|
"com_ui_filter_prompts_name": "Filter prompts by name",
|
||||||
"com_ui_finance": "Finance",
|
"com_ui_finance": "Finance",
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ export type BatchFile = {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
embedded: boolean;
|
embedded: boolean;
|
||||||
source: FileSources;
|
source: FileSources;
|
||||||
|
temp_file_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteFilesBody = {
|
export type DeleteFilesBody = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue