### What problem does this PR solve? Feat: Add FilesTable #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.16.0
| @@ -15,6 +15,7 @@ import weekYear from 'dayjs/plugin/weekYear'; | |||
| import weekday from 'dayjs/plugin/weekday'; | |||
| import React, { ReactNode, useEffect, useState } from 'react'; | |||
| import { ThemeProvider, useTheme } from './components/theme-provider'; | |||
| import { TooltipProvider } from './components/ui/tooltip'; | |||
| import storage from './utils/authorization-util'; | |||
| dayjs.extend(customParseFormat); | |||
| @@ -78,11 +79,13 @@ const RootProvider = ({ children }: React.PropsWithChildren) => { | |||
| }, []); | |||
| return ( | |||
| <QueryClientProvider client={queryClient}> | |||
| <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme"> | |||
| <Root>{children}</Root> | |||
| </ThemeProvider> | |||
| </QueryClientProvider> | |||
| <TooltipProvider> | |||
| <QueryClientProvider client={queryClient}> | |||
| <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme"> | |||
| <Root>{children}</Root> | |||
| </ThemeProvider> | |||
| </QueryClientProvider> | |||
| </TooltipProvider> | |||
| ); | |||
| }; | |||
| export function rootContainer(container: ReactNode) { | |||
| @@ -1,6 +1,7 @@ | |||
| import { Filter, Search } from 'lucide-react'; | |||
| import { Filter } from 'lucide-react'; | |||
| import { PropsWithChildren } from 'react'; | |||
| import { Button } from './ui/button'; | |||
| import { SearchInput } from './ui/input'; | |||
| interface IProps { | |||
| title: string; | |||
| @@ -17,7 +18,7 @@ export default function ListFilterBar({ | |||
| <span className="text-3xl font-bold ">{title}</span> | |||
| <div className="flex gap-4 items-center"> | |||
| <Filter className="size-5" /> | |||
| <Search className="size-5" /> | |||
| <SearchInput></SearchInput> | |||
| <Button variant={'tertiary'} size={'sm'} onClick={showDialog}> | |||
| {children} | |||
| </Button> | |||
| @@ -0,0 +1,13 @@ | |||
| import { Skeleton } from '@/components/ui/skeleton'; | |||
| export function SkeletonCard() { | |||
| return ( | |||
| <div className="flex flex-col space-y-3 items-center"> | |||
| <Skeleton className="h-[125px] w-[250px] rounded-xl" /> | |||
| <div className="space-y-2 w-[250px]"> | |||
| <Skeleton className="h-4 w-[250px]" /> | |||
| <Skeleton className="h-4 w-[200px]" /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| import { PropsWithChildren } from 'react'; | |||
| import { SkeletonCard } from './skeleton-card'; | |||
| import { TableCell, TableRow } from './ui/table'; | |||
| type IProps = { columnsLength: number }; | |||
| function Row({ children, columnsLength }: PropsWithChildren & IProps) { | |||
| return ( | |||
| <TableRow> | |||
| <TableCell colSpan={columnsLength} className="h-24 text-center "> | |||
| {children} | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| } | |||
| export function TableSkeleton({ columnsLength }: { columnsLength: number }) { | |||
| return ( | |||
| <Row columnsLength={columnsLength}> | |||
| <SkeletonCard></SkeletonCard> | |||
| </Row> | |||
| ); | |||
| } | |||
| export function TableEmpty({ columnsLength }: { columnsLength: number }) { | |||
| return <Row columnsLength={columnsLength}>No results.</Row>; | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Search } from 'lucide-react'; | |||
| export interface InputProps | |||
| extends React.InputHTMLAttributes<HTMLInputElement> {} | |||
| @@ -22,4 +23,38 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( | |||
| ); | |||
| Input.displayName = 'Input'; | |||
| export { Input }; | |||
| export interface ExpandedInputProps extends Omit<InputProps, 'prefix'> { | |||
| prefix?: React.ReactNode; | |||
| suffix?: React.ReactNode; | |||
| } | |||
| const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => { | |||
| return ( | |||
| <div className="relative"> | |||
| <span | |||
| className={cn({ | |||
| ['absolute left-3 top-[50%] translate-y-[-50%]']: prefix, | |||
| })} | |||
| > | |||
| {prefix} | |||
| </span> | |||
| <Input | |||
| className={cn({ 'pr-10': suffix, 'pl-10': prefix })} | |||
| {...props} | |||
| ></Input> | |||
| <span | |||
| className={cn({ | |||
| ['absolute right-3 top-[50%] translate-y-[-50%]']: suffix, | |||
| })} | |||
| > | |||
| {suffix} | |||
| </span> | |||
| </div> | |||
| ); | |||
| }; | |||
| const SearchInput = (props: InputProps) => { | |||
| return <ExpandedInput suffix={<Search />} {...props}></ExpandedInput>; | |||
| }; | |||
| export { ExpandedInput, Input, SearchInput }; | |||
| @@ -10,6 +10,7 @@ import { Routes } from '@/routes'; | |||
| import { | |||
| ChevronDown, | |||
| Cpu, | |||
| File, | |||
| Github, | |||
| House, | |||
| Library, | |||
| @@ -33,7 +34,7 @@ export function Header() { | |||
| { path: Routes.Chat, name: t('chat'), icon: MessageSquareText }, | |||
| { path: Routes.Search, name: t('search'), icon: Search }, | |||
| { path: Routes.Agent, name: t('flow'), icon: Cpu }, | |||
| // { path: '/file', name: t('fileManager'), icon: FileIcon }, | |||
| { path: Routes.Files, name: t('fileManager'), icon: File }, | |||
| ], | |||
| [t], | |||
| ); | |||
| @@ -13,7 +13,7 @@ export default function Dataset() { | |||
| documentUploadLoading, | |||
| } = useHandleUploadDocument(); | |||
| return ( | |||
| <section className="p-8 text-foreground"> | |||
| <section className="p-8"> | |||
| <ListFilterBar title="Files" showDialog={showDocumentUploadModal}> | |||
| <Upload /> | |||
| Upload file | |||
| @@ -50,7 +50,7 @@ export function InputForm() { | |||
| <Form {...form}> | |||
| <form | |||
| onSubmit={form.handleSubmit(onSubmit)} | |||
| className="w-2/3 space-y-6" | |||
| className="space-y-6" | |||
| id={FormId} | |||
| > | |||
| <FormField | |||
| @@ -1,5 +1,4 @@ | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useDownloadFile } from '@/hooks/file-manager-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| @@ -8,13 +7,13 @@ import { | |||
| isSupportedPreviewDocumentType, | |||
| } from '@/utils/document-util'; | |||
| import { | |||
| DeleteOutlined, | |||
| DownloadOutlined, | |||
| EditOutlined, | |||
| EyeOutlined, | |||
| LinkOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { Button, Space, Tooltip } from 'antd'; | |||
| import { FolderInput, Trash2 } from 'lucide-react'; | |||
| import { useHandleDeleteFile } from '../hooks'; | |||
| interface IProps { | |||
| @@ -92,15 +91,21 @@ const ActionCell = ({ | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={onShowMoveFileModal} | |||
| className="flex items-end" | |||
| > | |||
| <SvgIcon name={`move`} width={16}></SvgIcon> | |||
| <FolderInput className="size-4" /> | |||
| </Button> | |||
| </Tooltip> | |||
| )} | |||
| {isKnowledgeBase || ( | |||
| <Tooltip title={t('delete', { keyPrefix: 'common' })}> | |||
| <Button type="text" disabled={beingUsed} onClick={handleRemoveFile}> | |||
| <DeleteOutlined size={20} /> | |||
| <Button | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={handleRemoveFile} | |||
| className="flex items-end" | |||
| > | |||
| <Trash2 className="size-4" /> | |||
| </Button> | |||
| </Tooltip> | |||
| )} | |||
| @@ -1,5 +1,3 @@ | |||
| import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { | |||
| IListResult, | |||
| @@ -29,6 +27,7 @@ import { | |||
| useSelectBreadcrumbItems, | |||
| } from './hooks'; | |||
| import { FolderInput, Trash2 } from 'lucide-react'; | |||
| import styles from './index.less'; | |||
| interface IProps | |||
| @@ -127,8 +126,8 @@ const FileToolbar = ({ | |||
| onClick: handleRemoveFile, | |||
| label: ( | |||
| <Flex gap={10}> | |||
| <span className={styles.deleteIconWrapper}> | |||
| <DeleteIcon width={18} /> | |||
| <span className="flex items-center justify-center"> | |||
| <Trash2 className="size-4" /> | |||
| </span> | |||
| <b>{t('delete', { keyPrefix: 'common' })}</b> | |||
| </Flex> | |||
| @@ -139,8 +138,8 @@ const FileToolbar = ({ | |||
| onClick: handleShowMoveFileModal, | |||
| label: ( | |||
| <Flex gap={10}> | |||
| <span className={styles.deleteIconWrapper}> | |||
| <SvgIcon name={`move`} width={18}></SvgIcon> | |||
| <span className="flex items-center justify-center"> | |||
| <FolderInput className="size-4"></FolderInput> | |||
| </span> | |||
| <b>{t('move', { keyPrefix: 'common' })}</b> | |||
| </Flex> | |||
| @@ -0,0 +1,343 @@ | |||
| 'use client'; | |||
| import { | |||
| ColumnDef, | |||
| ColumnFiltersState, | |||
| SortingState, | |||
| VisibilityState, | |||
| flexRender, | |||
| getCoreRowModel, | |||
| getFilteredRowModel, | |||
| getSortedRowModel, | |||
| useReactTable, | |||
| } from '@tanstack/react-table'; | |||
| import { ArrowUpDown, MoreHorizontal, Pencil } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuSeparator, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableHead, | |||
| TableHeader, | |||
| TableRow, | |||
| } from '@/components/ui/table'; | |||
| import { | |||
| Tooltip, | |||
| TooltipContent, | |||
| TooltipTrigger, | |||
| } from '@/components/ui/tooltip'; | |||
| import { useFetchFileList } from '@/hooks/file-manager-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { formatFileSize } from '@/utils/common-util'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useNavigateToOtherFolder } from './hooks'; | |||
| export function FilesTable() { | |||
| const [sorting, setSorting] = React.useState<SortingState>([]); | |||
| const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | |||
| [], | |||
| ); | |||
| const [columnVisibility, setColumnVisibility] = | |||
| React.useState<VisibilityState>({}); | |||
| const [rowSelection, setRowSelection] = React.useState({}); | |||
| const { t } = useTranslation('translation', { | |||
| keyPrefix: 'fileManager', | |||
| }); | |||
| const navigateToOtherFolder = useNavigateToOtherFolder(); | |||
| const { pagination, data, loading, setPagination } = useFetchFileList(); | |||
| const columns: ColumnDef<IFile>[] = [ | |||
| { | |||
| id: 'select', | |||
| header: ({ table }) => ( | |||
| <Checkbox | |||
| checked={ | |||
| table.getIsAllPageRowsSelected() || | |||
| (table.getIsSomePageRowsSelected() && 'indeterminate') | |||
| } | |||
| onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} | |||
| aria-label="Select all" | |||
| /> | |||
| ), | |||
| cell: ({ row }) => ( | |||
| <Checkbox | |||
| checked={row.getIsSelected()} | |||
| onCheckedChange={(value) => row.toggleSelected(!!value)} | |||
| aria-label="Select row" | |||
| /> | |||
| ), | |||
| enableSorting: false, | |||
| enableHiding: false, | |||
| }, | |||
| { | |||
| accessorKey: 'name', | |||
| header: ({ column }) => { | |||
| return ( | |||
| <Button | |||
| variant="ghost" | |||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||
| > | |||
| {t('name')} | |||
| <ArrowUpDown /> | |||
| </Button> | |||
| ); | |||
| }, | |||
| meta: { cellClassName: 'max-w-[20vw]' }, | |||
| cell: ({ row }) => { | |||
| const name: string = row.getValue('name'); | |||
| const type = row.original.type; | |||
| const id = row.original.id; | |||
| const isFolder = type === 'folder'; | |||
| const handleNameClick = () => { | |||
| if (isFolder) { | |||
| navigateToOtherFolder(id); | |||
| } | |||
| }; | |||
| return ( | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <div className="flex gap-2"> | |||
| <SvgIcon | |||
| name={`file-icon/${isFolder ? 'folder' : getExtension(name)}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| <span | |||
| className={cn('truncate', { ['cursor-pointer']: isFolder })} | |||
| onClick={handleNameClick} | |||
| > | |||
| {name} | |||
| </span> | |||
| </div> | |||
| </TooltipTrigger> | |||
| <TooltipContent> | |||
| <p>{name}</p> | |||
| </TooltipContent> | |||
| </Tooltip> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| accessorKey: 'create_time', | |||
| header: ({ column }) => { | |||
| return ( | |||
| <Button | |||
| variant="ghost" | |||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||
| > | |||
| {t('uploadDate')} | |||
| <ArrowUpDown /> | |||
| </Button> | |||
| ); | |||
| }, | |||
| cell: ({ row }) => ( | |||
| <div className="lowercase"> | |||
| {formatDate(row.getValue('create_time'))} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| accessorKey: 'size', | |||
| header: ({ column }) => { | |||
| return ( | |||
| <Button | |||
| variant="ghost" | |||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||
| > | |||
| {t('size')} | |||
| <ArrowUpDown /> | |||
| </Button> | |||
| ); | |||
| }, | |||
| cell: ({ row }) => ( | |||
| <div className="capitalize">{formatFileSize(row.getValue('size'))}</div> | |||
| ), | |||
| }, | |||
| { | |||
| accessorKey: 'kbs_info', | |||
| header: t('knowledgeBase'), | |||
| cell: ({ row }) => ( | |||
| <Button variant="destructive" size={'sm'}> | |||
| {row.getValue('kbs_info')} | |||
| </Button> | |||
| ), | |||
| }, | |||
| { | |||
| id: 'actions', | |||
| header: t('action'), | |||
| enableHiding: false, | |||
| cell: ({ row }) => { | |||
| const payment = row.original; | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <Switch id="airplane-mode" /> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <Pencil /> | |||
| </Button> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <MoreHorizontal /> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuLabel>Actions</DropdownMenuLabel> | |||
| <DropdownMenuItem | |||
| onClick={() => navigator.clipboard.writeText(payment.id)} | |||
| > | |||
| Copy payment ID | |||
| </DropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <DropdownMenuItem>View customer</DropdownMenuItem> | |||
| <DropdownMenuItem>View payment details</DropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| </section> | |||
| ); | |||
| }, | |||
| }, | |||
| ]; | |||
| const currentPagination = useMemo(() => { | |||
| return { | |||
| pageIndex: (pagination.current || 1) - 1, | |||
| pageSize: pagination.pageSize || 10, | |||
| }; | |||
| }, [pagination]); | |||
| const table = useReactTable({ | |||
| data: data?.files || [], | |||
| columns, | |||
| onSortingChange: setSorting, | |||
| onColumnFiltersChange: setColumnFilters, | |||
| getCoreRowModel: getCoreRowModel(), | |||
| // getPaginationRowModel: getPaginationRowModel(), | |||
| getSortedRowModel: getSortedRowModel(), | |||
| getFilteredRowModel: getFilteredRowModel(), | |||
| onColumnVisibilityChange: setColumnVisibility, | |||
| onRowSelectionChange: setRowSelection, | |||
| onPaginationChange: (updaterOrValue: any) => { | |||
| if (typeof updaterOrValue === 'function') { | |||
| const nextPagination = updaterOrValue(currentPagination); | |||
| setPagination({ | |||
| page: nextPagination.pageIndex + 1, | |||
| pageSize: nextPagination.pageSize, | |||
| }); | |||
| } else { | |||
| setPagination({ | |||
| page: updaterOrValue.pageIndex, | |||
| pageSize: updaterOrValue.pageSize, | |||
| }); | |||
| } | |||
| }, | |||
| manualPagination: true, //we're doing manual "server-side" pagination | |||
| state: { | |||
| sorting, | |||
| columnFilters, | |||
| columnVisibility, | |||
| rowSelection, | |||
| pagination: currentPagination, | |||
| }, | |||
| rowCount: data?.total ?? 0, | |||
| debugTable: true, | |||
| }); | |||
| return ( | |||
| <div className="w-full"> | |||
| <div className="rounded-md border"> | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {loading ? ( | |||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||
| ) : table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell | |||
| key={cell.id} | |||
| className={cell.column.columnDef.meta?.cellClassName} | |||
| > | |||
| {flexRender( | |||
| cell.column.columnDef.cell, | |||
| cell.getContext(), | |||
| )} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| )) | |||
| ) : ( | |||
| <TableEmpty columnsLength={columns.length}></TableEmpty> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </div> | |||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||
| <div className="flex-1 text-sm text-muted-foreground"> | |||
| {table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '} | |||
| row(s) selected. | |||
| </div> | |||
| <div className="space-x-2"> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.previousPage()} | |||
| disabled={!table.getCanPreviousPage()} | |||
| > | |||
| Previous | |||
| </Button> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.nextPage()} | |||
| disabled={!table.getCanNextPage()} | |||
| > | |||
| Next | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,294 @@ | |||
| import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | |||
| import { | |||
| useConnectToKnowledge, | |||
| useCreateFolder, | |||
| useDeleteFile, | |||
| useFetchParentFolderList, | |||
| useMoveFile, | |||
| useRenameFile, | |||
| useUploadFile, | |||
| } from '@/hooks/file-manager-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { TableRowSelection } from 'antd/es/table/interface'; | |||
| import { UploadFile } from 'antd/lib'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { useNavigate, useSearchParams } from 'umi'; | |||
| export const useGetFolderId = () => { | |||
| const [searchParams] = useSearchParams(); | |||
| const id = searchParams.get('folderId') as string; | |||
| return id ?? ''; | |||
| }; | |||
| export const useGetRowSelection = () => { | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const rowSelection: TableRowSelection<IFile> = { | |||
| selectedRowKeys, | |||
| getCheckboxProps: (record) => { | |||
| return { disabled: record.source_type === 'knowledgebase' }; | |||
| }, | |||
| onChange: (newSelectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(newSelectedRowKeys); | |||
| }, | |||
| }; | |||
| return { rowSelection, setSelectedRowKeys }; | |||
| }; | |||
| export const useNavigateToOtherFolder = () => { | |||
| const navigate = useNavigate(); | |||
| const navigateToOtherFolder = useCallback( | |||
| (folderId: string) => { | |||
| navigate(`/file?folderId=${folderId}`); | |||
| }, | |||
| [navigate], | |||
| ); | |||
| return navigateToOtherFolder; | |||
| }; | |||
| export const useRenameCurrentFile = () => { | |||
| const [file, setFile] = useState<IFile>({} as IFile); | |||
| const { | |||
| visible: fileRenameVisible, | |||
| hideModal: hideFileRenameModal, | |||
| showModal: showFileRenameModal, | |||
| } = useSetModalState(); | |||
| const { renameFile, loading } = useRenameFile(); | |||
| const onFileRenameOk = useCallback( | |||
| async (name: string) => { | |||
| const ret = await renameFile({ | |||
| fileId: file.id, | |||
| name, | |||
| }); | |||
| if (ret === 0) { | |||
| hideFileRenameModal(); | |||
| } | |||
| }, | |||
| [renameFile, file, hideFileRenameModal], | |||
| ); | |||
| const handleShowFileRenameModal = useCallback( | |||
| async (record: IFile) => { | |||
| setFile(record); | |||
| showFileRenameModal(); | |||
| }, | |||
| [showFileRenameModal], | |||
| ); | |||
| return { | |||
| fileRenameLoading: loading, | |||
| initialFileName: file.name, | |||
| onFileRenameOk, | |||
| fileRenameVisible, | |||
| hideFileRenameModal, | |||
| showFileRenameModal: handleShowFileRenameModal, | |||
| }; | |||
| }; | |||
| export const useSelectBreadcrumbItems = () => { | |||
| const parentFolderList = useFetchParentFolderList(); | |||
| return parentFolderList.length === 1 | |||
| ? [] | |||
| : parentFolderList.map((x) => ({ | |||
| title: x.name === '/' ? 'root' : x.name, | |||
| path: `/file?folderId=${x.id}`, | |||
| })); | |||
| }; | |||
| export const useHandleCreateFolder = () => { | |||
| const { | |||
| visible: folderCreateModalVisible, | |||
| hideModal: hideFolderCreateModal, | |||
| showModal: showFolderCreateModal, | |||
| } = useSetModalState(); | |||
| const { createFolder, loading } = useCreateFolder(); | |||
| const id = useGetFolderId(); | |||
| const onFolderCreateOk = useCallback( | |||
| async (name: string) => { | |||
| const ret = await createFolder({ parentId: id, name }); | |||
| if (ret === 0) { | |||
| hideFolderCreateModal(); | |||
| } | |||
| }, | |||
| [createFolder, hideFolderCreateModal, id], | |||
| ); | |||
| return { | |||
| folderCreateLoading: loading, | |||
| onFolderCreateOk, | |||
| folderCreateModalVisible, | |||
| hideFolderCreateModal, | |||
| showFolderCreateModal, | |||
| }; | |||
| }; | |||
| export const useHandleDeleteFile = ( | |||
| fileIds: string[], | |||
| setSelectedRowKeys: (keys: string[]) => void, | |||
| ) => { | |||
| const { deleteFile: removeDocument } = useDeleteFile(); | |||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||
| const parentId = useGetFolderId(); | |||
| const handleRemoveFile = () => { | |||
| showDeleteConfirm({ | |||
| onOk: async () => { | |||
| const code = await removeDocument({ fileIds, parentId }); | |||
| if (code === 0) { | |||
| setSelectedRowKeys([]); | |||
| } | |||
| return; | |||
| }, | |||
| }); | |||
| }; | |||
| return { handleRemoveFile }; | |||
| }; | |||
| export const useHandleUploadFile = () => { | |||
| const { | |||
| visible: fileUploadVisible, | |||
| hideModal: hideFileUploadModal, | |||
| showModal: showFileUploadModal, | |||
| } = useSetModalState(); | |||
| const { uploadFile, loading } = useUploadFile(); | |||
| const id = useGetFolderId(); | |||
| const onFileUploadOk = useCallback( | |||
| async (fileList: UploadFile[]): Promise<number | undefined> => { | |||
| if (fileList.length > 0) { | |||
| const ret: number = await uploadFile({ fileList, parentId: id }); | |||
| if (ret === 0) { | |||
| hideFileUploadModal(); | |||
| } | |||
| return ret; | |||
| } | |||
| }, | |||
| [uploadFile, hideFileUploadModal, id], | |||
| ); | |||
| return { | |||
| fileUploadLoading: loading, | |||
| onFileUploadOk, | |||
| fileUploadVisible, | |||
| hideFileUploadModal, | |||
| showFileUploadModal, | |||
| }; | |||
| }; | |||
| export const useHandleConnectToKnowledge = () => { | |||
| const { | |||
| visible: connectToKnowledgeVisible, | |||
| hideModal: hideConnectToKnowledgeModal, | |||
| showModal: showConnectToKnowledgeModal, | |||
| } = useSetModalState(); | |||
| const { connectFileToKnowledge: connectToKnowledge, loading } = | |||
| useConnectToKnowledge(); | |||
| const [record, setRecord] = useState<IFile>({} as IFile); | |||
| const initialValue = useMemo(() => { | |||
| return Array.isArray(record?.kbs_info) | |||
| ? record?.kbs_info?.map((x) => x.kb_id) | |||
| : []; | |||
| }, [record?.kbs_info]); | |||
| const onConnectToKnowledgeOk = useCallback( | |||
| async (knowledgeIds: string[]) => { | |||
| const ret = await connectToKnowledge({ | |||
| fileIds: [record.id], | |||
| kbIds: knowledgeIds, | |||
| }); | |||
| if (ret === 0) { | |||
| hideConnectToKnowledgeModal(); | |||
| } | |||
| return ret; | |||
| }, | |||
| [connectToKnowledge, hideConnectToKnowledgeModal, record.id], | |||
| ); | |||
| const handleShowConnectToKnowledgeModal = useCallback( | |||
| (record: IFile) => { | |||
| setRecord(record); | |||
| showConnectToKnowledgeModal(); | |||
| }, | |||
| [showConnectToKnowledgeModal], | |||
| ); | |||
| return { | |||
| initialValue, | |||
| connectToKnowledgeLoading: loading, | |||
| onConnectToKnowledgeOk, | |||
| connectToKnowledgeVisible, | |||
| hideConnectToKnowledgeModal, | |||
| showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal, | |||
| }; | |||
| }; | |||
| export const useHandleBreadcrumbClick = () => { | |||
| const navigate = useNavigate(); | |||
| const handleBreadcrumbClick = useCallback( | |||
| (path?: string) => { | |||
| if (path) { | |||
| navigate(path); | |||
| } | |||
| }, | |||
| [navigate], | |||
| ); | |||
| return { handleBreadcrumbClick }; | |||
| }; | |||
| export const useHandleMoveFile = ( | |||
| setSelectedRowKeys: (keys: string[]) => void, | |||
| ) => { | |||
| const { | |||
| visible: moveFileVisible, | |||
| hideModal: hideMoveFileModal, | |||
| showModal: showMoveFileModal, | |||
| } = useSetModalState(); | |||
| const { moveFile, loading } = useMoveFile(); | |||
| const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | |||
| const onMoveFileOk = useCallback( | |||
| async (targetFolderId: string) => { | |||
| const ret = await moveFile({ | |||
| src_file_ids: sourceFileIds, | |||
| dest_file_id: targetFolderId, | |||
| }); | |||
| if (ret === 0) { | |||
| setSelectedRowKeys([]); | |||
| hideMoveFileModal(); | |||
| } | |||
| return ret; | |||
| }, | |||
| [moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys], | |||
| ); | |||
| const handleShowMoveFileModal = useCallback( | |||
| (ids: string[]) => { | |||
| setSourceFileIds(ids); | |||
| showMoveFileModal(); | |||
| }, | |||
| [showMoveFileModal], | |||
| ); | |||
| return { | |||
| initialValue: '', | |||
| moveFileLoading: loading, | |||
| onMoveFileOk, | |||
| moveFileVisible, | |||
| hideMoveFileModal, | |||
| showMoveFileModal: handleShowMoveFileModal, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,15 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Upload } from 'lucide-react'; | |||
| import { FilesTable } from './files-table'; | |||
| export default function Files() { | |||
| return ( | |||
| <section className="p-8"> | |||
| <ListFilterBar title="Files"> | |||
| <Upload /> | |||
| Upload file | |||
| </ListFilterBar> | |||
| <FilesTable></FilesTable> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -1,7 +1,6 @@ | |||
| import { | |||
| Tooltip, | |||
| TooltipContent, | |||
| TooltipProvider, | |||
| TooltipTrigger, | |||
| } from '@/components/ui/tooltip'; | |||
| import { | |||
| @@ -175,24 +174,20 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| <Background /> | |||
| <Controls> | |||
| <ControlButton onClick={handleImportJson}> | |||
| <TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <FolderInput className={controlIconClassname} /> | |||
| </TooltipTrigger> | |||
| <TooltipContent>Import</TooltipContent> | |||
| </Tooltip> | |||
| </TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <FolderInput className={controlIconClassname} /> | |||
| </TooltipTrigger> | |||
| <TooltipContent>Import</TooltipContent> | |||
| </Tooltip> | |||
| </ControlButton> | |||
| <ControlButton onClick={handleExportJson}> | |||
| <TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <FolderOutput className={controlIconClassname} /> | |||
| </TooltipTrigger> | |||
| <TooltipContent>Export</TooltipContent> | |||
| </Tooltip> | |||
| </TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <FolderOutput className={controlIconClassname} /> | |||
| </TooltipTrigger> | |||
| <TooltipContent>Export</TooltipContent> | |||
| </Tooltip> | |||
| </ControlButton> | |||
| </Controls> | |||
| </ReactFlow> | |||
| @@ -1,7 +1,6 @@ | |||
| import { | |||
| Tooltip, | |||
| TooltipContent, | |||
| TooltipProvider, | |||
| TooltipTrigger, | |||
| } from '@/components/ui/tooltip'; | |||
| import { PropsWithChildren } from 'react'; | |||
| @@ -10,13 +9,11 @@ import { useTranslation } from 'react-i18next'; | |||
| export const RunTooltip = ({ children }: PropsWithChildren) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger>{children}</TooltipTrigger> | |||
| <TooltipContent> | |||
| <p>{t('flow.testRun')}</p> | |||
| </TooltipContent> | |||
| </Tooltip> | |||
| </TooltipProvider> | |||
| <Tooltip> | |||
| <TooltipTrigger>{children}</TooltipTrigger> | |||
| <TooltipContent> | |||
| <p>{t('flow.testRun')}</p> | |||
| </TooltipContent> | |||
| </Tooltip> | |||
| ); | |||
| }; | |||
| @@ -40,7 +40,7 @@ const KnowledgeCard = ({ item }: IProps) => { | |||
| return ( | |||
| <Badge.Ribbon | |||
| text={item.nickname} | |||
| text={item?.nickname} | |||
| color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'} | |||
| className={classNames(styles.ribbon, { | |||
| [styles.hideRibbon]: item.permission !== 'team', | |||
| @@ -97,8 +97,8 @@ const Login = () => { | |||
| const step = Number((searchParams.get('step') ?? Step.SignIn) as Step); | |||
| return ( | |||
| <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')]"> | |||
| <div className="inline-block"> | |||
| <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center"> | |||
| <div className="inline-block bg-colors-background-neutral-standard rounded-lg"> | |||
| {step === Step.SignIn && <SignInCard></SignInCard>} | |||
| {step === Step.SignUp && <SignUpCard></SignUpCard>} | |||
| {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>} | |||
| @@ -7,6 +7,7 @@ export enum Routes { | |||
| Agent = '/agent', | |||
| Search = '/next-search', | |||
| Chat = '/next-chat', | |||
| Files = '/files', | |||
| ProfileSetting = '/profile-setting', | |||
| DatasetTesting = '/testing', | |||
| DatasetSetting = '/setting', | |||
| @@ -189,6 +190,17 @@ const routes = [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: Routes.Files, | |||
| layout: false, | |||
| component: '@/layouts/next', | |||
| routes: [ | |||
| { | |||
| path: Routes.Files, | |||
| component: `@/pages${Routes.Files}`, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: Routes.DatasetBase, | |||
| layout: false, | |||
| @@ -113,3 +113,28 @@ export function hexToArrayBuffer(input: string) { | |||
| return view.buffer; | |||
| } | |||
| export function formatFileSize(bytes: number, si = true, dp = 1) { | |||
| let nextBytes = bytes; | |||
| const thresh = si ? 1000 : 1024; | |||
| if (Math.abs(bytes) < thresh) { | |||
| return nextBytes + ' B'; | |||
| } | |||
| const units = si | |||
| ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] | |||
| : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; | |||
| let u = -1; | |||
| const r = 10 ** dp; | |||
| do { | |||
| nextBytes /= thresh; | |||
| ++u; | |||
| } while ( | |||
| Math.round(Math.abs(nextBytes) * r) / r >= thresh && | |||
| u < units.length - 1 | |||
| ); | |||
| return nextBytes.toFixed(dp) + ' ' + units[u]; | |||
| } | |||
| @@ -1,5 +1,13 @@ | |||
| import '@tanstack/react-table'; | |||
| declare module 'lodash'; | |||
| declare global { | |||
| type Nullable<T> = T | null; | |||
| } | |||
| declare module '@tanstack/react-table' { | |||
| interface ColumnMeta { | |||
| headerClassName?: string; | |||
| cellClassName?: string; | |||
| } | |||
| } | |||