### What problem does this PR solve? Feat: Create a folder #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| ChangeEventHandler, | ChangeEventHandler, | ||||
| FunctionComponent, | FunctionComponent, | ||||
| PropsWithChildren, | PropsWithChildren, | ||||
| ReactNode, | |||||
| } from 'react'; | } from 'react'; | ||||
| import { Button, ButtonProps } from './ui/button'; | import { Button, ButtonProps } from './ui/button'; | ||||
| import { SearchInput } from './ui/input'; | import { SearchInput } from './ui/input'; | ||||
| interface IProps { | interface IProps { | ||||
| title: string; | |||||
| showDialog?: () => void; | |||||
| title?: string; | |||||
| FilterPopover?: FunctionComponent<any>; | FilterPopover?: FunctionComponent<any>; | ||||
| searchString?: string; | searchString?: string; | ||||
| onSearchChange?: ChangeEventHandler<HTMLInputElement>; | onSearchChange?: ChangeEventHandler<HTMLInputElement>; | ||||
| count?: number; | count?: number; | ||||
| showFilter?: boolean; | showFilter?: boolean; | ||||
| leftPanel?: ReactNode; | |||||
| } | } | ||||
| const FilterButton = React.forwardRef< | const FilterButton = React.forwardRef< | ||||
| export default function ListFilterBar({ | export default function ListFilterBar({ | ||||
| title, | title, | ||||
| children, | children, | ||||
| showDialog, | |||||
| FilterPopover, | FilterPopover, | ||||
| searchString, | searchString, | ||||
| onSearchChange, | onSearchChange, | ||||
| count, | count, | ||||
| showFilter = true, | showFilter = true, | ||||
| leftPanel, | |||||
| }: PropsWithChildren<IProps>) { | }: PropsWithChildren<IProps>) { | ||||
| return ( | return ( | ||||
| <div className="flex justify-between mb-6"> | |||||
| <span className="text-3xl font-bold ">{title}</span> | |||||
| <div className="flex justify-between mb-6 items-center"> | |||||
| <span className="text-3xl font-bold ">{leftPanel || title}</span> | |||||
| <div className="flex gap-4 items-center"> | <div className="flex gap-4 items-center"> | ||||
| {showFilter && | {showFilter && | ||||
| (FilterPopover ? ( | (FilterPopover ? ( | ||||
| value={searchString} | value={searchString} | ||||
| onChange={onSearchChange} | onChange={onSearchChange} | ||||
| ></SearchInput> | ></SearchInput> | ||||
| <Button variant={'tertiary'} size={'sm'} onClick={showDialog}> | |||||
| {children} | |||||
| </Button> | |||||
| {children} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ); | ); |
| import { | |||||
| DropdownMenu, | |||||
| DropdownMenuContent, | |||||
| DropdownMenuItem, | |||||
| DropdownMenuTrigger, | |||||
| } from '@/components/ui/dropdown-menu'; | |||||
| import { ChevronDown, X } from 'lucide-react'; | |||||
| import React, { ReactNode, useState } from 'react'; | |||||
| export type TreeNode = { | |||||
| id: number; | |||||
| label: ReactNode; | |||||
| children?: TreeNode[]; | |||||
| }; | |||||
| type SingleSelectTreeDropdownProps = { | |||||
| allowDelete?: boolean; | |||||
| treeData: TreeNode[]; | |||||
| }; | |||||
| const SingleTreeSelect: React.FC<SingleSelectTreeDropdownProps> = ({ | |||||
| allowDelete = false, | |||||
| treeData, | |||||
| }) => { | |||||
| const [selectedOption, setSelectedOption] = useState<TreeNode | null>(null); | |||||
| const handleSelect = (option: TreeNode) => { | |||||
| setSelectedOption(option); | |||||
| }; | |||||
| const handleDelete = (e: React.MouseEvent<HTMLButtonElement>) => { | |||||
| e.stopPropagation(); | |||||
| console.log( | |||||
| 'Delete button clicked. Current selected option:', | |||||
| selectedOption, | |||||
| ); | |||||
| setSelectedOption(null); | |||||
| console.log('After deletion, selected option:', selectedOption); | |||||
| }; | |||||
| const renderTree = (nodes: TreeNode[]) => { | |||||
| return nodes.map((node) => ( | |||||
| <div key={node.id} className="pl-4"> | |||||
| <DropdownMenuItem | |||||
| onClick={() => handleSelect(node)} | |||||
| className={`flex items-center ${ | |||||
| selectedOption?.id === node.id ? 'bg-gray-500' : '' | |||||
| }`} | |||||
| > | |||||
| <span>{node.label}</span> | |||||
| {node.children && ( | |||||
| <ChevronDown className="ml-2 h-4 w-4 text-gray-400" /> | |||||
| )} | |||||
| </DropdownMenuItem> | |||||
| {node.children && renderTree(node.children)} | |||||
| </div> | |||||
| )); | |||||
| }; | |||||
| return ( | |||||
| <div className="relative"> | |||||
| <DropdownMenu> | |||||
| <DropdownMenuTrigger asChild> | |||||
| <button | |||||
| type="button" | |||||
| className="flex items-center justify-between space-x-1 p-2 border rounded-md focus:outline-none w-full" | |||||
| > | |||||
| {selectedOption ? ( | |||||
| <> | |||||
| <span>{selectedOption.label}</span> | |||||
| {allowDelete && ( | |||||
| <button | |||||
| type="button" | |||||
| className="ml-2 text-gray-500 hover:text-red-500 focus:outline-none" | |||||
| onClick={handleDelete} | |||||
| > | |||||
| <X className="h-4 w-4" /> | |||||
| </button> | |||||
| )} | |||||
| </> | |||||
| ) : ( | |||||
| 'Select an option' | |||||
| )} | |||||
| <ChevronDown className="h-4 w-4" /> | |||||
| </button> | |||||
| </DropdownMenuTrigger> | |||||
| <DropdownMenuContent className=" mt-2 w-56 origin-top-right rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> | |||||
| {renderTree(treeData)} | |||||
| </DropdownMenuContent> | |||||
| </DropdownMenu> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default SingleTreeSelect; |
| [getQueryString, id, navigate], | [getQueryString, id, navigate], | ||||
| ); | ); | ||||
| const navigateToFiles = useCallback( | |||||
| (folderId?: string) => { | |||||
| navigate(`${Routes.Files}?folderId=${folderId}`); | |||||
| }, | |||||
| [navigate], | |||||
| ); | |||||
| return { | return { | ||||
| navigateToDatasetList, | navigateToDatasetList, | ||||
| navigateToDataset, | navigateToDataset, | ||||
| navigateToAgentTemplates, | navigateToAgentTemplates, | ||||
| navigateToSearchList, | navigateToSearchList, | ||||
| navigateToSearch, | navigateToSearch, | ||||
| navigateToFiles, | |||||
| }; | }; | ||||
| }; | }; |
| import { IFolder } from '@/interfaces/database/file-manager'; | |||||
| import fileManagerService from '@/services/file-manager-service'; | import fileManagerService from '@/services/file-manager-service'; | ||||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||||
| import { message } from 'antd'; | import { message } from 'antd'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useSearchParams } from 'umi'; | |||||
| import { useSetPaginationParams } from './route-hook'; | import { useSetPaginationParams } from './route-hook'; | ||||
| export const enum FileApiAction { | export const enum FileApiAction { | ||||
| UploadFile = 'uploadFile', | UploadFile = 'uploadFile', | ||||
| FetchFileList = 'fetchFileList', | FetchFileList = 'fetchFileList', | ||||
| MoveFile = 'moveFile', | |||||
| CreateFolder = 'createFolder', | |||||
| FetchParentFolderList = 'fetchParentFolderList', | |||||
| } | } | ||||
| export const useGetFolderId = () => { | |||||
| const [searchParams] = useSearchParams(); | |||||
| const id = searchParams.get('folderId') as string; | |||||
| return id ?? ''; | |||||
| }; | |||||
| export const useUploadFile = () => { | export const useUploadFile = () => { | ||||
| const { setPaginationParams } = useSetPaginationParams(); | const { setPaginationParams } = useSetPaginationParams(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| return { data, loading, uploadFile: mutateAsync }; | return { data, loading, uploadFile: mutateAsync }; | ||||
| }; | }; | ||||
| export interface IMoveFileBody { | |||||
| src_file_ids: string[]; | |||||
| dest_file_id: string; // target folder id | |||||
| } | |||||
| export const useMoveFile = () => { | |||||
| const queryClient = useQueryClient(); | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [FileApiAction.MoveFile], | |||||
| mutationFn: async (params: IMoveFileBody) => { | |||||
| const { data } = await fileManagerService.moveFile(params); | |||||
| if (data.code === 0) { | |||||
| message.success(t('message.operated')); | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: [FileApiAction.FetchFileList], | |||||
| }); | |||||
| } | |||||
| return data.code; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, moveFile: mutateAsync }; | |||||
| }; | |||||
| export const useCreateFolder = () => { | |||||
| const { setPaginationParams } = useSetPaginationParams(); | |||||
| const queryClient = useQueryClient(); | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [FileApiAction.CreateFolder], | |||||
| mutationFn: async (params: { parentId: string; name: string }) => { | |||||
| const { data } = await fileManagerService.createFolder({ | |||||
| ...params, | |||||
| type: 'folder', | |||||
| }); | |||||
| if (data.code === 0) { | |||||
| message.success(t('message.created')); | |||||
| setPaginationParams(1); | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: [FileApiAction.FetchFileList], | |||||
| }); | |||||
| } | |||||
| return data.code; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, createFolder: mutateAsync }; | |||||
| }; | |||||
| export const useFetchParentFolderList = () => { | |||||
| const id = useGetFolderId(); | |||||
| const { data } = useQuery<IFolder[]>({ | |||||
| queryKey: [FileApiAction.FetchParentFolderList, id], | |||||
| initialData: [], | |||||
| enabled: !!id, | |||||
| queryFn: async () => { | |||||
| const { data } = await fileManagerService.getAllParentFolder({ | |||||
| fileId: id, | |||||
| }); | |||||
| return data?.data?.parent_folders?.toReversed() ?? []; | |||||
| }, | |||||
| }); | |||||
| return data; | |||||
| }; |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useFetchFlowList } from '@/hooks/flow-hooks'; | import { useFetchFlowList } from '@/hooks/flow-hooks'; | ||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | ||||
| import { Plus } from 'lucide-react'; | import { Plus } from 'lucide-react'; | ||||
| return ( | return ( | ||||
| <section> | <section> | ||||
| <div className="px-8 pt-8"> | <div className="px-8 pt-8"> | ||||
| <ListFilterBar title="Agents" showDialog={navigateToAgentTemplates}> | |||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| <ListFilterBar title="Agents"> | |||||
| <Button | |||||
| variant={'tertiary'} | |||||
| size={'sm'} | |||||
| onClick={navigateToAgentTemplates} | |||||
| > | |||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| </Button> | |||||
| </ListFilterBar> | </ListFilterBar> | ||||
| </div> | </div> | ||||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8"> | <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8"> |
| import { FileUploadDialog } from '@/components/file-upload-dialog'; | import { FileUploadDialog } from '@/components/file-upload-dialog'; | ||||
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { Upload } from 'lucide-react'; | import { Upload } from 'lucide-react'; | ||||
| import { DatasetTable } from './dataset-table'; | import { DatasetTable } from './dataset-table'; | ||||
| import { useHandleUploadDocument } from './use-upload-document'; | import { useHandleUploadDocument } from './use-upload-document'; | ||||
| } = useHandleUploadDocument(); | } = useHandleUploadDocument(); | ||||
| return ( | return ( | ||||
| <section className="p-8"> | <section className="p-8"> | ||||
| <ListFilterBar title="Files" showDialog={showDocumentUploadModal}> | |||||
| <Upload /> | |||||
| Upload file | |||||
| <ListFilterBar title="Files"> | |||||
| <Button | |||||
| variant={'tertiary'} | |||||
| size={'sm'} | |||||
| onClick={showDocumentUploadModal} | |||||
| > | |||||
| <Upload /> | |||||
| Upload file | |||||
| </Button> | |||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <DatasetTable></DatasetTable> | <DatasetTable></DatasetTable> | ||||
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { RenameDialog } from '@/components/rename-dialog'; | import { RenameDialog } from '@/components/rename-dialog'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | ||||
| import { pick } from 'lodash'; | import { pick } from 'lodash'; | ||||
| import { Plus } from 'lucide-react'; | import { Plus } from 'lucide-react'; | ||||
| <section className="p-8 text-foreground"> | <section className="p-8 text-foreground"> | ||||
| <ListFilterBar | <ListFilterBar | ||||
| title="Datasets" | title="Datasets" | ||||
| showDialog={showModal} | |||||
| count={ownerIds.length} | count={ownerIds.length} | ||||
| FilterPopover={({ children }: PropsWithChildren) => ( | FilterPopover={({ children }: PropsWithChildren) => ( | ||||
| <DatasetsFilterPopover setOwnerIds={setOwnerIds} ownerIds={ownerIds}> | <DatasetsFilterPopover setOwnerIds={setOwnerIds} ownerIds={ownerIds}> | ||||
| searchString={searchString} | searchString={searchString} | ||||
| onSearchChange={handleInputChange} | onSearchChange={handleInputChange} | ||||
| > | > | ||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| <Button variant={'tertiary'} size={'sm'} onClick={showModal}> | |||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| </Button> | |||||
| Create dataset | Create dataset | ||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> |
| UseHandleConnectToKnowledgeReturnType, | UseHandleConnectToKnowledgeReturnType, | ||||
| UseRenameCurrentFileReturnType, | UseRenameCurrentFileReturnType, | ||||
| } from './hooks'; | } from './hooks'; | ||||
| import { UseMoveDocumentReturnType } from './use-move-file'; | |||||
| type IProps = Pick<CellContext<IFile, unknown>, 'row'> & | type IProps = Pick<CellContext<IFile, unknown>, 'row'> & | ||||
| Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'> & | Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'> & | ||||
| Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'>; | |||||
| Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'> & | |||||
| Pick<UseMoveDocumentReturnType, 'showMoveFileModal'>; | |||||
| export function ActionCell({ | export function ActionCell({ | ||||
| row, | row, | ||||
| showConnectToKnowledgeModal, | showConnectToKnowledgeModal, | ||||
| showFileRenameModal, | showFileRenameModal, | ||||
| showMoveFileModal, | |||||
| }: IProps) { | }: IProps) { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const record = row.original; | const record = row.original; | ||||
| showFileRenameModal(record); | showFileRenameModal(record); | ||||
| }, [record, showFileRenameModal]); | }, [record, showFileRenameModal]); | ||||
| const handleShowMoveFileModal = useCallback(() => { | |||||
| showMoveFileModal([record.id]); | |||||
| }, [record, showMoveFileModal]); | |||||
| return ( | return ( | ||||
| <section className="flex gap-4 items-center"> | <section className="flex gap-4 items-center"> | ||||
| <Button | <Button | ||||
| </Button> | </Button> | ||||
| </DropdownMenuTrigger> | </DropdownMenuTrigger> | ||||
| <DropdownMenuContent align="end"> | <DropdownMenuContent align="end"> | ||||
| <DropdownMenuItem | |||||
| onClick={() => navigator.clipboard.writeText(record.id)} | |||||
| > | |||||
| <DropdownMenuItem onClick={handleShowMoveFileModal}> | |||||
| {t('common.move')} | {t('common.move')} | ||||
| </DropdownMenuItem> | </DropdownMenuItem> | ||||
| <DropdownMenuSeparator /> | <DropdownMenuSeparator /> |
| 'use client'; | |||||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||||
| import { useForm } from 'react-hook-form'; | |||||
| import { z } from 'zod'; | |||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Input } from '@/components/ui/input'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { TagRenameId } from '@/pages/add-knowledge/constant'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| export function CreateFolderForm({ hideModal, onOk }: IModalProps<any>) { | |||||
| const { t } = useTranslation(); | |||||
| const FormSchema = z.object({ | |||||
| name: z | |||||
| .string() | |||||
| .min(1, { | |||||
| message: t('common.namePlaceholder'), | |||||
| }) | |||||
| .trim(), | |||||
| }); | |||||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||||
| resolver: zodResolver(FormSchema), | |||||
| defaultValues: { name: '' }, | |||||
| }); | |||||
| async function onSubmit(data: z.infer<typeof FormSchema>) { | |||||
| const ret = await onOk?.(data.name); | |||||
| if (ret) { | |||||
| hideModal?.(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <Form {...form}> | |||||
| <form | |||||
| onSubmit={form.handleSubmit(onSubmit)} | |||||
| className="space-y-6" | |||||
| id={TagRenameId} | |||||
| > | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="name" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('common.name')}</FormLabel> | |||||
| <FormControl> | |||||
| <Input | |||||
| placeholder={t('common.namePlaceholder')} | |||||
| {...field} | |||||
| autoComplete="off" | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </form> | |||||
| </Form> | |||||
| ); | |||||
| } |
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogFooter, | |||||
| DialogHeader, | |||||
| DialogTitle, | |||||
| } from '@/components/ui/dialog'; | |||||
| import { LoadingButton } from '@/components/ui/loading-button'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { TagRenameId } from '@/pages/add-knowledge/constant'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { CreateFolderForm } from './create-folder-form'; | |||||
| export function CreateFolderDialog({ | |||||
| hideModal, | |||||
| onOk, | |||||
| loading, | |||||
| }: IModalProps<any>) { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <Dialog open onOpenChange={hideModal}> | |||||
| <DialogContent className="sm:max-w-[425px]"> | |||||
| <DialogHeader> | |||||
| <DialogTitle>{t('fileManager.newFolder')}</DialogTitle> | |||||
| </DialogHeader> | |||||
| <CreateFolderForm hideModal={hideModal} onOk={onOk}></CreateFolderForm> | |||||
| <DialogFooter> | |||||
| <LoadingButton type="submit" form={TagRenameId} loading={loading}> | |||||
| {t('common.save')} | |||||
| </LoadingButton> | |||||
| </DialogFooter> | |||||
| </DialogContent> | |||||
| </Dialog> | |||||
| ); | |||||
| } |
| import { | |||||
| Breadcrumb, | |||||
| BreadcrumbItem, | |||||
| BreadcrumbList, | |||||
| BreadcrumbPage, | |||||
| BreadcrumbSeparator, | |||||
| } from '@/components/ui/breadcrumb'; | |||||
| import { useNavigate } from 'umi'; | |||||
| import { useSelectBreadcrumbItems } from './use-navigate-to-folder'; | |||||
| export function FileBreadcrumb() { | |||||
| const breadcrumbItems = useSelectBreadcrumbItems(); | |||||
| const navigate = useNavigate(); | |||||
| return ( | |||||
| <Breadcrumb> | |||||
| <BreadcrumbList> | |||||
| {breadcrumbItems.map((x, idx) => ( | |||||
| <div key={x.path} className="flex items-center gap-2"> | |||||
| {idx !== 0 && <BreadcrumbSeparator />} | |||||
| <BreadcrumbItem | |||||
| key={x.path} | |||||
| onClick={() => navigate(x.path)} | |||||
| className="cursor-pointer" | |||||
| > | |||||
| {idx === breadcrumbItems.length - 1 ? ( | |||||
| <BreadcrumbPage>{x.title}</BreadcrumbPage> | |||||
| ) : ( | |||||
| x.title | |||||
| )} | |||||
| </BreadcrumbItem> | |||||
| </div> | |||||
| ))} | |||||
| </BreadcrumbList> | |||||
| </Breadcrumb> | |||||
| ); | |||||
| } |
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { ActionCell } from './action-cell'; | import { ActionCell } from './action-cell'; | ||||
| import { | |||||
| useHandleConnectToKnowledge, | |||||
| useNavigateToOtherFolder, | |||||
| useRenameCurrentFile, | |||||
| } from './hooks'; | |||||
| import { useHandleConnectToKnowledge, useRenameCurrentFile } from './hooks'; | |||||
| import { LinkToDatasetDialog } from './link-to-dataset-dialog'; | import { LinkToDatasetDialog } from './link-to-dataset-dialog'; | ||||
| import { MoveDialog } from './move-dialog'; | |||||
| import { useHandleMoveFile } from './use-move-file'; | |||||
| import { useNavigateToOtherFolder } from './use-navigate-to-folder'; | |||||
| export function FilesTable() { | export function FilesTable() { | ||||
| const [sorting, setSorting] = React.useState<SortingState>([]); | const [sorting, setSorting] = React.useState<SortingState>([]); | ||||
| fileRenameLoading, | fileRenameLoading, | ||||
| } = useRenameCurrentFile(); | } = useRenameCurrentFile(); | ||||
| const { | |||||
| showMoveFileModal, | |||||
| moveFileVisible, | |||||
| onMoveFileOk, | |||||
| hideMoveFileModal, | |||||
| moveFileLoading, | |||||
| } = useHandleMoveFile(); | |||||
| const { pagination, data, loading, setPagination } = useFetchFileList(); | const { pagination, data, loading, setPagination } = useFetchFileList(); | ||||
| const columns: ColumnDef<IFile>[] = [ | const columns: ColumnDef<IFile>[] = [ | ||||
| row={row} | row={row} | ||||
| showConnectToKnowledgeModal={showConnectToKnowledgeModal} | showConnectToKnowledgeModal={showConnectToKnowledgeModal} | ||||
| showFileRenameModal={showFileRenameModal} | showFileRenameModal={showFileRenameModal} | ||||
| showMoveFileModal={showMoveFileModal} | |||||
| ></ActionCell> | ></ActionCell> | ||||
| ); | ); | ||||
| }, | }, | ||||
| loading={fileRenameLoading} | loading={fileRenameLoading} | ||||
| ></RenameDialog> | ></RenameDialog> | ||||
| )} | )} | ||||
| {moveFileVisible && ( | |||||
| <MoveDialog | |||||
| hideModal={hideMoveFileModal} | |||||
| onOk={onMoveFileOk} | |||||
| loading={moveFileLoading} | |||||
| ></MoveDialog> | |||||
| )} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } |
| import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| useConnectToKnowledge, | useConnectToKnowledge, | ||||
| useCreateFolder, | |||||
| useDeleteFile, | useDeleteFile, | ||||
| useFetchParentFolderList, | |||||
| useMoveFile, | |||||
| useRenameFile, | useRenameFile, | ||||
| } from '@/hooks/file-manager-hooks'; | } from '@/hooks/file-manager-hooks'; | ||||
| import { IFile } from '@/interfaces/database/file-manager'; | import { IFile } from '@/interfaces/database/file-manager'; | ||||
| return { rowSelection, setSelectedRowKeys }; | return { rowSelection, setSelectedRowKeys }; | ||||
| }; | }; | ||||
| export const useNavigateToOtherFolder = () => { | |||||
| const navigate = useNavigate(); | |||||
| const navigateToOtherFolder = useCallback( | |||||
| (folderId: string) => { | |||||
| navigate(`/file?folderId=${folderId}`); | |||||
| }, | |||||
| [navigate], | |||||
| ); | |||||
| return navigateToOtherFolder; | |||||
| }; | |||||
| export const useRenameCurrentFile = () => { | export const useRenameCurrentFile = () => { | ||||
| const [file, setFile] = useState<IFile>({} as IFile); | const [file, setFile] = useState<IFile>({} as IFile); | ||||
| const { | const { | ||||
| typeof useRenameCurrentFile | typeof useRenameCurrentFile | ||||
| >; | >; | ||||
| 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 = ( | export const useHandleDeleteFile = ( | ||||
| fileIds: string[], | fileIds: string[], | ||||
| setSelectedRowKeys: (keys: string[]) => void, | setSelectedRowKeys: (keys: string[]) => void, | ||||
| return { handleBreadcrumbClick }; | 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, | |||||
| }; | |||||
| }; |
| import { FileUploadDialog } from '@/components/file-upload-dialog'; | import { FileUploadDialog } from '@/components/file-upload-dialog'; | ||||
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { | |||||
| DropdownMenu, | |||||
| DropdownMenuContent, | |||||
| DropdownMenuItem, | |||||
| DropdownMenuSeparator, | |||||
| DropdownMenuTrigger, | |||||
| } from '@/components/ui/dropdown-menu'; | |||||
| import { Upload } from 'lucide-react'; | import { Upload } from 'lucide-react'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { CreateFolderDialog } from './create-folder-dialog'; | |||||
| import { FileBreadcrumb } from './file-breadcrumb'; | |||||
| import { FilesTable } from './files-table'; | import { FilesTable } from './files-table'; | ||||
| import { useHandleCreateFolder } from './use-create-folder'; | |||||
| import { useHandleUploadFile } from './use-upload-file'; | import { useHandleUploadFile } from './use-upload-file'; | ||||
| export default function Files() { | export default function Files() { | ||||
| const { t } = useTranslation(); | |||||
| const { | const { | ||||
| fileUploadVisible, | fileUploadVisible, | ||||
| hideFileUploadModal, | hideFileUploadModal, | ||||
| onFileUploadOk, | onFileUploadOk, | ||||
| } = useHandleUploadFile(); | } = useHandleUploadFile(); | ||||
| const { | |||||
| folderCreateModalVisible, | |||||
| showFolderCreateModal, | |||||
| hideFolderCreateModal, | |||||
| folderCreateLoading, | |||||
| onFolderCreateOk, | |||||
| } = useHandleCreateFolder(); | |||||
| const leftPanel = ( | |||||
| <div> | |||||
| <FileBreadcrumb></FileBreadcrumb> | |||||
| </div> | |||||
| ); | |||||
| return ( | return ( | ||||
| <section className="p-8"> | <section className="p-8"> | ||||
| <ListFilterBar title="Files" showDialog={showFileUploadModal}> | |||||
| <Upload /> | |||||
| Upload file | |||||
| <ListFilterBar leftPanel={leftPanel}> | |||||
| <DropdownMenu> | |||||
| <DropdownMenuTrigger asChild> | |||||
| <Button variant={'tertiary'} size={'sm'}> | |||||
| <Upload /> | |||||
| {t('knowledgeDetails.addFile')} | |||||
| </Button> | |||||
| </DropdownMenuTrigger> | |||||
| <DropdownMenuContent className="w-56"> | |||||
| <DropdownMenuItem onClick={showFileUploadModal}> | |||||
| {t('fileManager.uploadFile')} | |||||
| </DropdownMenuItem> | |||||
| <DropdownMenuSeparator /> | |||||
| <DropdownMenuItem onClick={showFolderCreateModal}> | |||||
| {t('fileManager.newFolder')} | |||||
| </DropdownMenuItem> | |||||
| </DropdownMenuContent> | |||||
| </DropdownMenu> | |||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <FilesTable></FilesTable> | <FilesTable></FilesTable> | ||||
| {fileUploadVisible && ( | {fileUploadVisible && ( | ||||
| loading={fileUploadLoading} | loading={fileUploadLoading} | ||||
| ></FileUploadDialog> | ></FileUploadDialog> | ||||
| )} | )} | ||||
| {folderCreateModalVisible && ( | |||||
| <CreateFolderDialog | |||||
| loading={folderCreateLoading} | |||||
| visible={folderCreateModalVisible} | |||||
| hideModal={hideFolderCreateModal} | |||||
| onOk={onFolderCreateOk} | |||||
| ></CreateFolderDialog> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| } | } |
| import { Button } from '@/components/ui/button'; | |||||
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogFooter, | |||||
| DialogHeader, | |||||
| DialogTitle, | |||||
| } from '@/components/ui/dialog'; | |||||
| import SingleTreeSelect, { TreeNode } from '@/components/ui/single-tree-select'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| export function MoveDialog({ hideModal }: IModalProps<any>) { | |||||
| const { t } = useTranslation(); | |||||
| const treeData: TreeNode[] = [ | |||||
| { | |||||
| id: 1, | |||||
| label: 'Node 1', | |||||
| children: [ | |||||
| { id: 11, label: 'Node 1.1' }, | |||||
| { id: 12, label: 'Node 1.2' }, | |||||
| ], | |||||
| }, | |||||
| { | |||||
| id: 2, | |||||
| label: 'Node 2', | |||||
| children: [ | |||||
| { | |||||
| id: 21, | |||||
| label: 'Node 2.1', | |||||
| children: [ | |||||
| { id: 211, label: 'Node 2.1.1' }, | |||||
| { id: 212, label: 'Node 2.1.2' }, | |||||
| ], | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <Dialog open onOpenChange={hideModal}> | |||||
| <DialogContent> | |||||
| <DialogHeader> | |||||
| <DialogTitle>{t('common.move')}</DialogTitle> | |||||
| </DialogHeader> | |||||
| <div> | |||||
| <SingleTreeSelect treeData={treeData}></SingleTreeSelect> | |||||
| </div> | |||||
| <DialogFooter> | |||||
| <Button type="submit">Save changes</Button> | |||||
| </DialogFooter> | |||||
| </DialogContent> | |||||
| </Dialog> | |||||
| ); | |||||
| } |
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useCreateFolder } from '@/hooks/use-file-request'; | |||||
| import { useCallback } from 'react'; | |||||
| import { useGetFolderId } from './hooks'; | |||||
| 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, | |||||
| }; | |||||
| }; |
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useMoveFile } from '@/hooks/use-file-request'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| 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], | |||||
| ); | |||||
| const handleShowMoveFileModal = useCallback( | |||||
| (ids: string[]) => { | |||||
| setSourceFileIds(ids); | |||||
| showMoveFileModal(); | |||||
| }, | |||||
| [showMoveFileModal], | |||||
| ); | |||||
| return { | |||||
| initialValue: '', | |||||
| moveFileLoading: loading, | |||||
| onMoveFileOk, | |||||
| moveFileVisible, | |||||
| hideMoveFileModal, | |||||
| showMoveFileModal: handleShowMoveFileModal, | |||||
| }; | |||||
| }; | |||||
| export type UseMoveDocumentReturnType = ReturnType<typeof useHandleMoveFile>; |
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||||
| import { useFetchParentFolderList } from '@/hooks/use-file-request'; | |||||
| import { Routes } from '@/routes'; | |||||
| import { useCallback } from 'react'; | |||||
| export const useNavigateToOtherFolder = () => { | |||||
| const { navigateToFiles } = useNavigatePage(); | |||||
| const navigateToOtherFolder = useCallback( | |||||
| (folderId: string) => { | |||||
| navigateToFiles(folderId); | |||||
| }, | |||||
| [navigateToFiles], | |||||
| ); | |||||
| return navigateToOtherFolder; | |||||
| }; | |||||
| export const useSelectBreadcrumbItems = () => { | |||||
| const parentFolderList = useFetchParentFolderList(); | |||||
| return parentFolderList.length === 1 | |||||
| ? [] | |||||
| : parentFolderList.map((x) => ({ | |||||
| title: x.name === '/' ? 'root' : x.name, | |||||
| path: `${Routes.Files}?folderId=${x.id}`, | |||||
| })); | |||||
| }; |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useFetchChatAppList } from '@/hooks/chat-hooks'; | import { useFetchChatAppList } from '@/hooks/chat-hooks'; | ||||
| import { Plus } from 'lucide-react'; | import { Plus } from 'lucide-react'; | ||||
| import { ChatCard } from './chat-card'; | import { ChatCard } from './chat-card'; | ||||
| return ( | return ( | ||||
| <section className="p-8"> | <section className="p-8"> | ||||
| <ListFilterBar title="Chat apps"> | <ListFilterBar title="Chat apps"> | ||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| <Button variant={'tertiary'} size={'sm'}> | |||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| </Button> | |||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | ||||
| {chatList.map((x) => { | {chatList.map((x) => { |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useFetchFlowList } from '@/hooks/flow-hooks'; | import { useFetchFlowList } from '@/hooks/flow-hooks'; | ||||
| import { Plus } from 'lucide-react'; | import { Plus } from 'lucide-react'; | ||||
| import { SearchCard } from './search-card'; | import { SearchCard } from './search-card'; | ||||
| <section> | <section> | ||||
| <div className="px-8 pt-8"> | <div className="px-8 pt-8"> | ||||
| <ListFilterBar title="Search apps"> | <ListFilterBar title="Search apps"> | ||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| <Button variant={'tertiary'} size={'sm'}> | |||||
| <Plus className="mr-2 h-4 w-4" /> | |||||
| Create app | |||||
| </Button> | |||||
| </ListFilterBar> | </ListFilterBar> | ||||
| </div> | </div> | ||||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8"> | <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8"> |