From 62f3cf060429220de8c6ed126dcd978f050084a4 Mon Sep 17 00:00:00 2001 From: agatha197 Date: Tue, 25 Feb 2025 16:43:56 +0900 Subject: [PATCH] feat(FR-592): NEO vfolder page - delete folders --- .../components/BAIConfirmModalWithInput.tsx | 93 ++++++++ react/src/components/DeleteVFolderModal.tsx | 104 +++++++++ react/src/components/FolderCreateModal.tsx | 5 +- react/src/components/VFolderNodes.tsx | 218 ++++++++++++++++-- react/src/helper/index.tsx | 4 + react/src/hooks/index.tsx | 3 + react/src/pages/VFolderNodeListPage.tsx | 75 +++--- resources/i18n/de.json | 4 +- resources/i18n/el.json | 5 +- resources/i18n/en.json | 4 +- resources/i18n/es.json | 4 +- resources/i18n/fi.json | 4 +- resources/i18n/fr.json | 4 +- resources/i18n/id.json | 4 +- resources/i18n/it.json | 4 +- resources/i18n/ja.json | 4 +- resources/i18n/ko.json | 2 + resources/i18n/mn.json | 4 +- resources/i18n/ms.json | 4 +- resources/i18n/pl.json | 4 +- resources/i18n/pt-BR.json | 4 +- resources/i18n/pt.json | 4 +- resources/i18n/ru.json | 4 +- resources/i18n/th.json | 4 +- resources/i18n/tr.json | 4 +- resources/i18n/vi.json | 4 +- resources/i18n/zh-CN.json | 4 +- resources/i18n/zh-TW.json | 4 +- 28 files changed, 500 insertions(+), 85 deletions(-) create mode 100644 react/src/components/BAIConfirmModalWithInput.tsx create mode 100644 react/src/components/DeleteVFolderModal.tsx diff --git a/react/src/components/BAIConfirmModalWithInput.tsx b/react/src/components/BAIConfirmModalWithInput.tsx new file mode 100644 index 0000000000..37b662c5ca --- /dev/null +++ b/react/src/components/BAIConfirmModalWithInput.tsx @@ -0,0 +1,93 @@ +import BAIModal, { BAIModalProps } from './BAIModal'; +import Flex from './Flex'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { Form, Input, Typography } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +const { Text } = Typography; + +interface BAIConfirmModalWithInputProps extends Omit { + confirmText: string; + content: React.ReactNode; + title: React.ReactNode; + icon?: React.ReactNode; +} + +const BAIConfirmModalWithInput: React.FC = ({ + confirmText, + title, + content, + icon, + onOk, + onCancel, + ...props +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const typedText = Form.useWatch('confirmText', form); + + return ( + + + {icon ?? ( + + )} + {title} + + + } + onOk={(e) => { + form.resetFields(); + _.isFunction(onOk) && onOk(e); + }} + onCancel={(e) => { + form.resetFields(); + _.isFunction(onCancel) && onCancel(e); + }} + okButtonProps={{ disabled: confirmText !== typedText, danger: true }} + {...props} + > + + {content} +
+ { + if (value === confirmText) { + return Promise.resolve(); + } + return Promise.reject(); + }, + }, + ]} + > + { + e.preventDefault(); + e.stopPropagation(); + }} + /> + +
+
+
+ ); +}; + +export default BAIConfirmModalWithInput; diff --git a/react/src/components/DeleteVFolderModal.tsx b/react/src/components/DeleteVFolderModal.tsx new file mode 100644 index 0000000000..b3b59064d7 --- /dev/null +++ b/react/src/components/DeleteVFolderModal.tsx @@ -0,0 +1,104 @@ +import { toLocalId } from '../helper'; +import { useSuspendedBackendaiClient } from '../hooks'; +import { useTanMutation } from '../hooks/reactQueryAlias'; +import { useSetBAINotification } from '../hooks/useBAINotification'; +import { usePainKiller } from '../hooks/usePainKiller'; +import BAIModal, { BAIModalProps } from './BAIModal'; +import { DeleteVFolderModalFragment$key } from './__generated__/DeleteVFolderModalFragment.graphql'; +import { VFolderNodesFragment$data } from './__generated__/VFolderNodesFragment.graphql'; +import { Typography, message } from 'antd'; +import graphql from 'babel-plugin-relay/macro'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFragment } from 'react-relay'; + +type VFolderType = NonNullable; + +interface DeleteVFolderModalProps extends BAIModalProps { + vfolderFrgmts?: DeleteVFolderModalFragment$key; + onRequestClose?: (success: boolean) => void; +} + +const DeleteVFolderModal: React.FC = ({ + vfolderFrgmts, + onRequestClose, + ...baiModalProps +}) => { + const { t } = useTranslation(); + const { upsertNotification } = useSetBAINotification(); + const baiClient = useSuspendedBackendaiClient(); + const painKiller = usePainKiller(); + + const vfolders = useFragment( + graphql` + fragment DeleteVFolderModalFragment on VirtualFolderNode + @relay(plural: true) { + id + name + } + `, + vfolderFrgmts, + ); + + const deleteMutation = useTanMutation({ + mutationFn: (id: string) => { + return baiClient.vfolder.delete_by_id(toLocalId(id)); + }, + }); + + return ( + onRequestClose?.(false)} + onOk={() => { + const promises = _.map(vfolders, (vfolder: VFolderType) => + deleteMutation.mutateAsync(vfolder.id).catch((error) => { + upsertNotification({ + message: painKiller.relieve(error?.title), + description: error?.description, + open: true, + }); + }), + ); + Promise.allSettled(promises).then((results) => { + const success = results.every( + (result) => result.status === 'fulfilled', + ); + if (success) { + if (vfolders?.length === 1) { + message.success( + t('data.folders.FolderDeleted', { + folderName: vfolders?.[0]?.name, + }), + ); + } else { + message.success( + t('data.folders.MultipleFolderDeleted', { + folderLength: vfolders?.length, + }), + ); + } + } + onRequestClose?.(success); + }); + }} + {...baiModalProps} + > + + {vfolders?.length === 1 + ? t('data.folders.MoveToTrashDescription', { + folderName: vfolders?.[0]?.name, + }) + : t('data.folders.MoveToTrashMultipleDescription', { + folderLength: vfolders?.length, + })} + + + ); +}; + +export default DeleteVFolderModal; diff --git a/react/src/components/FolderCreateModal.tsx b/react/src/components/FolderCreateModal.tsx index 605efe4c18..47462e267a 100644 --- a/react/src/components/FolderCreateModal.tsx +++ b/react/src/components/FolderCreateModal.tsx @@ -56,7 +56,7 @@ interface FolderCreateFormItemsType { group: string | undefined; usage_mode: 'general' | 'model'; type: 'user' | 'project'; - permission: 'rw' | 'ro' | 'wd'; + permission: 'rw' | 'ro'; cloneable: boolean; } @@ -69,7 +69,7 @@ export interface FolderCreationResponse { quota_scope_id: string; host: string; usage_mode: 'general' | 'model'; - permission: 'rw' | 'ro' | 'wd'; + permission: 'rw' | 'ro'; max_size: number; creator: string; ownership_type: 'user' | 'project'; @@ -303,7 +303,6 @@ const FolderCreateModal: React.FC = ({ Read & Write Read Only - Delete diff --git a/react/src/components/VFolderNodes.tsx b/react/src/components/VFolderNodes.tsx index 2ac1f3ffdb..0ae1641f8f 100644 --- a/react/src/components/VFolderNodes.tsx +++ b/react/src/components/VFolderNodes.tsx @@ -1,6 +1,13 @@ -import { filterNonNullItems } from '../helper'; +import { filterNonNullItems, toLocalId } from '../helper'; +import { useSuspendedBackendaiClient } from '../hooks'; import { useCurrentUserInfo } from '../hooks/backendai'; +import { useTanMutation } from '../hooks/reactQueryAlias'; +import { useSetBAINotification } from '../hooks/useBAINotification'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; +import { usePainKiller } from '../hooks/usePainKiller'; +import { isDeletedCategory } from '../pages/VFolderNodeListPage'; +import BAIConfirmModalWithInput from './BAIConfirmModalWithInput'; +import RestoreIcon from './BAIIcons/RestoreIcon'; import ShareAltIcon from './BAIIcons/ShareAltIcon'; import TrashBinIcon from './BAIIcons/TrashBinIcon'; import UserUnionIcon from './BAIIcons/UserUnionIcon'; @@ -14,10 +21,10 @@ import { VFolderNodesFragment$key, } from './__generated__/VFolderNodesFragment.graphql'; import { CheckCircleOutlined, UserOutlined } from '@ant-design/icons'; -import { Button, theme, Typography } from 'antd'; +import { Alert, App, Button, Popconfirm, theme, Typography } from 'antd'; import graphql from 'babel-plugin-relay/macro'; import _ from 'lodash'; -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useFragment } from 'react-relay'; @@ -41,23 +48,32 @@ interface VFolderNodesProps extends Omit, 'dataSource' | 'columns'> { vfoldersFrgmt: VFolderNodesFragment$key; onClickVFolderName?: (vfolder: VFolderNodeInList) => void; + onRequestChange?: () => void; } const VFolderNodes: React.FC = ({ vfoldersFrgmt, onClickVFolderName, + onRequestChange, ...tableProps }) => { const { t } = useTranslation(); const { token } = theme.useToken(); + const { message } = App.useApp(); + const currentProject = useCurrentProjectValue(); + const baiClient = useSuspendedBackendaiClient(); + const painKiller = usePainKiller(); + const { upsertNotification } = useSetBAINotification(); const [currentUser] = useCurrentUserInfo(); + const [currentVFolder, setCurrentVFolder] = + useState(null); + const vfolders = useFragment( graphql` fragment VFolderNodesFragment on VirtualFolderNode @relay(plural: true) { id @required(action: NONE) - row_id @required(action: NONE) status name host @@ -69,9 +85,26 @@ const VFolderNodes: React.FC = ({ `, vfoldersFrgmt, ); - const filteredVFolders = filterNonNullItems(vfolders); + const deleteMutation = useTanMutation({ + mutationFn: (id: string) => { + return baiClient.vfolder.delete_by_id(toLocalId(id)); + }, + }); + + const restoreMutation = useTanMutation({ + mutationFn: (id: string) => { + return baiClient.vfolder.restore_from_trash_bin(toLocalId(id)); + }, + }); + + const deleteFromTrashBinMutation = useTanMutation({ + mutationFn: (id: string) => { + return baiClient.vfolder.delete_from_trash_bin(toLocalId(id)); + }, + }); + return ( <> @@ -79,8 +112,6 @@ const VFolderNodes: React.FC = ({ neoStyle showSorterTooltip={false} sortDirections={['descend', 'ascend', 'descend']} - // TODO: fix type - // @ts-ignore rowKey={(record) => record.id} size="small" dataSource={filteredVFolders} @@ -128,24 +159,113 @@ const VFolderNodes: React.FC = ({ render: (__, vfolder) => { return ( -