### 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
| @@ -3,18 +3,19 @@ import React, { | |||
| ChangeEventHandler, | |||
| FunctionComponent, | |||
| PropsWithChildren, | |||
| ReactNode, | |||
| } from 'react'; | |||
| import { Button, ButtonProps } from './ui/button'; | |||
| import { SearchInput } from './ui/input'; | |||
| interface IProps { | |||
| title: string; | |||
| showDialog?: () => void; | |||
| title?: string; | |||
| FilterPopover?: FunctionComponent<any>; | |||
| searchString?: string; | |||
| onSearchChange?: ChangeEventHandler<HTMLInputElement>; | |||
| count?: number; | |||
| showFilter?: boolean; | |||
| leftPanel?: ReactNode; | |||
| } | |||
| const FilterButton = React.forwardRef< | |||
| @@ -31,16 +32,16 @@ const FilterButton = React.forwardRef< | |||
| export default function ListFilterBar({ | |||
| title, | |||
| children, | |||
| showDialog, | |||
| FilterPopover, | |||
| searchString, | |||
| onSearchChange, | |||
| count, | |||
| showFilter = true, | |||
| leftPanel, | |||
| }: PropsWithChildren<IProps>) { | |||
| 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"> | |||
| {showFilter && | |||
| (FilterPopover ? ( | |||
| @@ -55,9 +56,7 @@ export default function ListFilterBar({ | |||
| value={searchString} | |||
| onChange={onSearchChange} | |||
| ></SearchInput> | |||
| <Button variant={'tertiary'} size={'sm'} onClick={showDialog}> | |||
| {children} | |||
| </Button> | |||
| {children} | |||
| </div> | |||
| </div> | |||
| ); | |||
| @@ -0,0 +1,95 @@ | |||
| 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; | |||
| @@ -94,6 +94,13 @@ export const useNavigatePage = () => { | |||
| [getQueryString, id, navigate], | |||
| ); | |||
| const navigateToFiles = useCallback( | |||
| (folderId?: string) => { | |||
| navigate(`${Routes.Files}?folderId=${folderId}`); | |||
| }, | |||
| [navigate], | |||
| ); | |||
| return { | |||
| navigateToDatasetList, | |||
| navigateToDataset, | |||
| @@ -109,5 +116,6 @@ export const useNavigatePage = () => { | |||
| navigateToAgentTemplates, | |||
| navigateToSearchList, | |||
| navigateToSearch, | |||
| navigateToFiles, | |||
| }; | |||
| }; | |||
| @@ -1,14 +1,26 @@ | |||
| import { IFolder } from '@/interfaces/database/file-manager'; | |||
| 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 { useTranslation } from 'react-i18next'; | |||
| import { useSearchParams } from 'umi'; | |||
| import { useSetPaginationParams } from './route-hook'; | |||
| export const enum FileApiAction { | |||
| UploadFile = 'uploadFile', | |||
| 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 = () => { | |||
| const { setPaginationParams } = useSetPaginationParams(); | |||
| const { t } = useTranslation(); | |||
| @@ -46,3 +58,81 @@ export const useUploadFile = () => { | |||
| 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; | |||
| }; | |||
| @@ -1,4 +1,5 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useFetchFlowList } from '@/hooks/flow-hooks'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| import { Plus } from 'lucide-react'; | |||
| @@ -11,9 +12,15 @@ export default function Agent() { | |||
| return ( | |||
| <section> | |||
| <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> | |||
| </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"> | |||
| @@ -1,5 +1,6 @@ | |||
| import { FileUploadDialog } from '@/components/file-upload-dialog'; | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Upload } from 'lucide-react'; | |||
| import { DatasetTable } from './dataset-table'; | |||
| import { useHandleUploadDocument } from './use-upload-document'; | |||
| @@ -14,9 +15,15 @@ export default function Dataset() { | |||
| } = useHandleUploadDocument(); | |||
| return ( | |||
| <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> | |||
| <DatasetTable></DatasetTable> | |||
| @@ -1,5 +1,6 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | |||
| import { pick } from 'lodash'; | |||
| import { Plus } from 'lucide-react'; | |||
| @@ -51,7 +52,6 @@ export default function Datasets() { | |||
| <section className="p-8 text-foreground"> | |||
| <ListFilterBar | |||
| title="Datasets" | |||
| showDialog={showModal} | |||
| count={ownerIds.length} | |||
| FilterPopover={({ children }: PropsWithChildren) => ( | |||
| <DatasetsFilterPopover setOwnerIds={setOwnerIds} ownerIds={ownerIds}> | |||
| @@ -61,7 +61,9 @@ export default function Datasets() { | |||
| searchString={searchString} | |||
| 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 | |||
| </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"> | |||
| @@ -17,15 +17,18 @@ import { | |||
| UseHandleConnectToKnowledgeReturnType, | |||
| UseRenameCurrentFileReturnType, | |||
| } from './hooks'; | |||
| import { UseMoveDocumentReturnType } from './use-move-file'; | |||
| type IProps = Pick<CellContext<IFile, unknown>, 'row'> & | |||
| Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'> & | |||
| Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'>; | |||
| Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'> & | |||
| Pick<UseMoveDocumentReturnType, 'showMoveFileModal'>; | |||
| export function ActionCell({ | |||
| row, | |||
| showConnectToKnowledgeModal, | |||
| showFileRenameModal, | |||
| showMoveFileModal, | |||
| }: IProps) { | |||
| const { t } = useTranslation(); | |||
| const record = row.original; | |||
| @@ -47,6 +50,10 @@ export function ActionCell({ | |||
| showFileRenameModal(record); | |||
| }, [record, showFileRenameModal]); | |||
| const handleShowMoveFileModal = useCallback(() => { | |||
| showMoveFileModal([record.id]); | |||
| }, [record, showMoveFileModal]); | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <Button | |||
| @@ -68,9 +75,7 @@ export function ActionCell({ | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuItem | |||
| onClick={() => navigator.clipboard.writeText(record.id)} | |||
| > | |||
| <DropdownMenuItem onClick={handleShowMoveFileModal}> | |||
| {t('common.move')} | |||
| </DropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| @@ -0,0 +1,70 @@ | |||
| '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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -42,12 +42,11 @@ import { getExtension } from '@/utils/document-util'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { ActionCell } from './action-cell'; | |||
| import { | |||
| useHandleConnectToKnowledge, | |||
| useNavigateToOtherFolder, | |||
| useRenameCurrentFile, | |||
| } from './hooks'; | |||
| import { useHandleConnectToKnowledge, useRenameCurrentFile } from './hooks'; | |||
| 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() { | |||
| const [sorting, setSorting] = React.useState<SortingState>([]); | |||
| @@ -78,6 +77,14 @@ export function FilesTable() { | |||
| fileRenameLoading, | |||
| } = useRenameCurrentFile(); | |||
| const { | |||
| showMoveFileModal, | |||
| moveFileVisible, | |||
| onMoveFileOk, | |||
| hideMoveFileModal, | |||
| moveFileLoading, | |||
| } = useHandleMoveFile(); | |||
| const { pagination, data, loading, setPagination } = useFetchFileList(); | |||
| const columns: ColumnDef<IFile>[] = [ | |||
| @@ -222,6 +229,7 @@ export function FilesTable() { | |||
| row={row} | |||
| showConnectToKnowledgeModal={showConnectToKnowledgeModal} | |||
| showFileRenameModal={showFileRenameModal} | |||
| showMoveFileModal={showMoveFileModal} | |||
| ></ActionCell> | |||
| ); | |||
| }, | |||
| @@ -363,6 +371,13 @@ export function FilesTable() { | |||
| loading={fileRenameLoading} | |||
| ></RenameDialog> | |||
| )} | |||
| {moveFileVisible && ( | |||
| <MoveDialog | |||
| hideModal={hideMoveFileModal} | |||
| onOk={onMoveFileOk} | |||
| loading={moveFileLoading} | |||
| ></MoveDialog> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -1,10 +1,7 @@ | |||
| import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | |||
| import { | |||
| useConnectToKnowledge, | |||
| useCreateFolder, | |||
| useDeleteFile, | |||
| useFetchParentFolderList, | |||
| useMoveFile, | |||
| useRenameFile, | |||
| } from '@/hooks/file-manager-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| @@ -35,18 +32,6 @@ export const useGetRowSelection = () => { | |||
| 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 { | |||
| @@ -92,46 +77,6 @@ export type UseRenameCurrentFileReturnType = ReturnType< | |||
| 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 = ( | |||
| fileIds: string[], | |||
| setSelectedRowKeys: (keys: string[]) => void, | |||
| @@ -222,48 +167,3 @@ export const useHandleBreadcrumbClick = () => { | |||
| 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, | |||
| }; | |||
| }; | |||
| @@ -1,10 +1,23 @@ | |||
| import { FileUploadDialog } from '@/components/file-upload-dialog'; | |||
| 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 { useTranslation } from 'react-i18next'; | |||
| import { CreateFolderDialog } from './create-folder-dialog'; | |||
| import { FileBreadcrumb } from './file-breadcrumb'; | |||
| import { FilesTable } from './files-table'; | |||
| import { useHandleCreateFolder } from './use-create-folder'; | |||
| import { useHandleUploadFile } from './use-upload-file'; | |||
| export default function Files() { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| fileUploadVisible, | |||
| hideFileUploadModal, | |||
| @@ -13,11 +26,40 @@ export default function Files() { | |||
| onFileUploadOk, | |||
| } = useHandleUploadFile(); | |||
| const { | |||
| folderCreateModalVisible, | |||
| showFolderCreateModal, | |||
| hideFolderCreateModal, | |||
| folderCreateLoading, | |||
| onFolderCreateOk, | |||
| } = useHandleCreateFolder(); | |||
| const leftPanel = ( | |||
| <div> | |||
| <FileBreadcrumb></FileBreadcrumb> | |||
| </div> | |||
| ); | |||
| return ( | |||
| <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> | |||
| <FilesTable></FilesTable> | |||
| {fileUploadVisible && ( | |||
| @@ -27,6 +69,14 @@ export default function Files() { | |||
| loading={fileUploadLoading} | |||
| ></FileUploadDialog> | |||
| )} | |||
| {folderCreateModalVisible && ( | |||
| <CreateFolderDialog | |||
| loading={folderCreateLoading} | |||
| visible={folderCreateModalVisible} | |||
| hideModal={hideFolderCreateModal} | |||
| onOk={onFolderCreateOk} | |||
| ></CreateFolderDialog> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| 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, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,50 @@ | |||
| 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>; | |||
| @@ -0,0 +1,28 @@ | |||
| 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}`, | |||
| })); | |||
| }; | |||
| @@ -1,4 +1,5 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useFetchChatAppList } from '@/hooks/chat-hooks'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { ChatCard } from './chat-card'; | |||
| @@ -9,8 +10,10 @@ export default function ChatList() { | |||
| return ( | |||
| <section className="p-8"> | |||
| <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> | |||
| <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) => { | |||
| @@ -1,4 +1,5 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useFetchFlowList } from '@/hooks/flow-hooks'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { SearchCard } from './search-card'; | |||
| @@ -10,8 +11,10 @@ export default function SearchList() { | |||
| <section> | |||
| <div className="px-8 pt-8"> | |||
| <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> | |||
| </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"> | |||