From 51d9bde5a342c07aab18d8ffbb2b5b058ccbbf79 Mon Sep 17 00:00:00 2001 From: balibabu Date: Wed, 23 Apr 2025 15:21:09 +0800 Subject: [PATCH] Feat: Create a folder #3221 (#7228) ### What problem does this PR solve? Feat: Create a folder #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/list-filter-bar.tsx | 15 ++- web/src/components/ui/single-tree-select.tsx | 95 +++++++++++++++++ web/src/hooks/logic-hooks/navigate-hooks.ts | 8 ++ web/src/hooks/use-file-request.ts | 92 +++++++++++++++- web/src/pages/agents/index.tsx | 13 ++- web/src/pages/dataset/dataset/index.tsx | 13 ++- web/src/pages/datasets/index.tsx | 6 +- web/src/pages/files/action-cell.tsx | 13 ++- .../create-folder-form.tsx | 70 ++++++++++++ .../files/create-folder-dialog/index.tsx | 36 +++++++ web/src/pages/files/file-breadcrumb.tsx | 36 +++++++ web/src/pages/files/files-table.tsx | 25 ++++- web/src/pages/files/hooks.ts | 100 ------------------ web/src/pages/files/index.tsx | 56 +++++++++- web/src/pages/files/move-dialog.tsx | 56 ++++++++++ web/src/pages/files/use-create-folder.ts | 33 ++++++ web/src/pages/files/use-move-file.ts | 50 +++++++++ web/src/pages/files/use-navigate-to-folder.ts | 28 +++++ web/src/pages/next-chats/index.tsx | 7 +- web/src/pages/next-searches/index.tsx | 7 +- 20 files changed, 626 insertions(+), 133 deletions(-) create mode 100644 web/src/components/ui/single-tree-select.tsx create mode 100644 web/src/pages/files/create-folder-dialog/create-folder-form.tsx create mode 100644 web/src/pages/files/create-folder-dialog/index.tsx create mode 100644 web/src/pages/files/file-breadcrumb.tsx create mode 100644 web/src/pages/files/move-dialog.tsx create mode 100644 web/src/pages/files/use-create-folder.ts create mode 100644 web/src/pages/files/use-move-file.ts create mode 100644 web/src/pages/files/use-navigate-to-folder.ts diff --git a/web/src/components/list-filter-bar.tsx b/web/src/components/list-filter-bar.tsx index aa04b9b05..20795ddbc 100644 --- a/web/src/components/list-filter-bar.tsx +++ b/web/src/components/list-filter-bar.tsx @@ -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; searchString?: string; onSearchChange?: ChangeEventHandler; 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) { return ( -
- {title} +
+ {leftPanel || title}
{showFilter && (FilterPopover ? ( @@ -55,9 +56,7 @@ export default function ListFilterBar({ value={searchString} onChange={onSearchChange} > - + {children}
); diff --git a/web/src/components/ui/single-tree-select.tsx b/web/src/components/ui/single-tree-select.tsx new file mode 100644 index 000000000..87acf6959 --- /dev/null +++ b/web/src/components/ui/single-tree-select.tsx @@ -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 = ({ + allowDelete = false, + treeData, +}) => { + const [selectedOption, setSelectedOption] = useState(null); + + const handleSelect = (option: TreeNode) => { + setSelectedOption(option); + }; + + const handleDelete = (e: React.MouseEvent) => { + 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) => ( +
+ handleSelect(node)} + className={`flex items-center ${ + selectedOption?.id === node.id ? 'bg-gray-500' : '' + }`} + > + {node.label} + {node.children && ( + + )} + + {node.children && renderTree(node.children)} +
+ )); + }; + + return ( +
+ + + + )} + + ) : ( + 'Select an option' + )} + + + + + {renderTree(treeData)} + + +
+ ); +}; + +export default SingleTreeSelect; diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index ef6f1559e..5cdff81b9 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -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, }; }; diff --git a/web/src/hooks/use-file-request.ts b/web/src/hooks/use-file-request.ts index deaccac30..34bdbbf85 100644 --- a/web/src/hooks/use-file-request.ts +++ b/web/src/hooks/use-file-request.ts @@ -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({ + queryKey: [FileApiAction.FetchParentFolderList, id], + initialData: [], + enabled: !!id, + queryFn: async () => { + const { data } = await fileManagerService.getAllParentFolder({ + fileId: id, + }); + + return data?.data?.parent_folders?.toReversed() ?? []; + }, + }); + + return data; +}; diff --git a/web/src/pages/agents/index.tsx b/web/src/pages/agents/index.tsx index f568dc629..117275e27 100644 --- a/web/src/pages/agents/index.tsx +++ b/web/src/pages/agents/index.tsx @@ -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 (
- - - Create app + +
diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index 7cb612c42..3bcc99a9b 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/web/src/pages/dataset/dataset/index.tsx @@ -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 (
- - - Upload file + + diff --git a/web/src/pages/datasets/index.tsx b/web/src/pages/datasets/index.tsx index 7ed2d2500..e1db3cebb 100644 --- a/web/src/pages/datasets/index.tsx +++ b/web/src/pages/datasets/index.tsx @@ -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() {
( @@ -61,7 +61,9 @@ export default function Datasets() { searchString={searchString} onSearchChange={handleInputChange} > - + Create dataset
diff --git a/web/src/pages/files/action-cell.tsx b/web/src/pages/files/action-cell.tsx index d91422fe2..f6b7d4663 100644 --- a/web/src/pages/files/action-cell.tsx +++ b/web/src/pages/files/action-cell.tsx @@ -17,15 +17,18 @@ import { UseHandleConnectToKnowledgeReturnType, UseRenameCurrentFileReturnType, } from './hooks'; +import { UseMoveDocumentReturnType } from './use-move-file'; type IProps = Pick, 'row'> & Pick & - Pick; + Pick & + Pick; 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 (
); } diff --git a/web/src/pages/files/hooks.ts b/web/src/pages/files/hooks.ts index 42c03c496..99fedb024 100644 --- a/web/src/pages/files/hooks.ts +++ b/web/src/pages/files/hooks.ts @@ -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({} 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([]); - - 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, - }; -}; diff --git a/web/src/pages/files/index.tsx b/web/src/pages/files/index.tsx index 706522bc8..3226f6a08 100644 --- a/web/src/pages/files/index.tsx +++ b/web/src/pages/files/index.tsx @@ -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 = ( +
+ +
+ ); + return (
- - - Upload file + + + + + + + + {t('fileManager.uploadFile')} + + + + {t('fileManager.newFolder')} + + + {fileUploadVisible && ( @@ -27,6 +69,14 @@ export default function Files() { loading={fileUploadLoading} > )} + {folderCreateModalVisible && ( + + )}
); } diff --git a/web/src/pages/files/move-dialog.tsx b/web/src/pages/files/move-dialog.tsx new file mode 100644 index 000000000..2e3cdc354 --- /dev/null +++ b/web/src/pages/files/move-dialog.tsx @@ -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) { + 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 ( + + + + {t('common.move')} + +
+ +
+ + + +
+
+ ); +} diff --git a/web/src/pages/files/use-create-folder.ts b/web/src/pages/files/use-create-folder.ts new file mode 100644 index 000000000..899160edf --- /dev/null +++ b/web/src/pages/files/use-create-folder.ts @@ -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, + }; +}; diff --git a/web/src/pages/files/use-move-file.ts b/web/src/pages/files/use-move-file.ts new file mode 100644 index 000000000..5b2294a0d --- /dev/null +++ b/web/src/pages/files/use-move-file.ts @@ -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([]); + + 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; diff --git a/web/src/pages/files/use-navigate-to-folder.ts b/web/src/pages/files/use-navigate-to-folder.ts new file mode 100644 index 000000000..883b587fb --- /dev/null +++ b/web/src/pages/files/use-navigate-to-folder.ts @@ -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}`, + })); +}; diff --git a/web/src/pages/next-chats/index.tsx b/web/src/pages/next-chats/index.tsx index 11a6b101d..6c649ca04 100644 --- a/web/src/pages/next-chats/index.tsx +++ b/web/src/pages/next-chats/index.tsx @@ -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 (
- - Create app +
{chatList.map((x) => { diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index a9ea4907e..ff7962b6b 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -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() {
- - Create app +