diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js index 44c84c8632d..ac485ffc50c 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -60,6 +60,8 @@ const propTypes = { onItemSelected: PropTypes.func.isRequired, onItemDelete: PropTypes.func.isRequired, onItemRename: PropTypes.func.isRequired, + deleteFilesCallback: PropTypes.func, + renameFileCallback: PropTypes.func, onItemMove: PropTypes.func.isRequired, onItemCopy: PropTypes.func.isRequired, onItemConvert: PropTypes.func.isRequired, @@ -198,6 +200,8 @@ class DirColumnView extends React.Component { repoID={this.props.repoID} repoInfo={this.props.currentRepoInfo} viewID={this.props.viewId} + deleteFilesCallback={this.props.deleteFilesCallback} + renameFileCallback={this.props.renameFileCallback} /> } {currentMode === LIST_MODE && diff --git a/frontend/src/components/tree-view/tree-helper.js b/frontend/src/components/tree-view/tree-helper.js index 1fb85fcf36c..22445b74c9c 100644 --- a/frontend/src/components/tree-view/tree-helper.js +++ b/frontend/src/components/tree-view/tree-helper.js @@ -68,6 +68,9 @@ class TreeHelper { renameNodeByPath(tree, nodePath, newName) { let treeCopy = tree.clone(); let node = treeCopy.getNodeByPath(nodePath); + if (!node) { + return treeCopy; + } treeCopy.renameNode(node, newName); return treeCopy; } diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 927db70bc4d..f04b1e2c996 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -239,11 +239,11 @@ class MetadataManagerAPI { * @param {string[]} dirents - Array of file/folder paths to delete * @returns {Promise} Axios delete request promise */ - deleteImages(repoID, dirents) { + batchDeleteFiles(repo_id, file_names) { const url = this.server + '/api/v2.1/repos/batch-delete-folders-item/'; const data = { - repo_id: repoID, - file_names: dirents + repo_id, + file_names, }; return this.req.delete(url, { data }); } diff --git a/frontend/src/metadata/components/cell-editors/editor-container/index.js b/frontend/src/metadata/components/cell-editors/editor-container/index.js index a61592f31cc..72c51606ccc 100644 --- a/frontend/src/metadata/components/cell-editors/editor-container/index.js +++ b/frontend/src/metadata/components/cell-editors/editor-container/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import NormalEditorContainer from './normal-editor-container'; import PopupEditorContainer from './popup-editor-container'; import PreviewEditorContainer from './preview-editor-container'; -import { CellType } from '../../../constants'; +import { CellType, EDITOR_TYPE } from '../../../constants'; const POPUP_EDITOR_COLUMN_TYPES = [ CellType.DATE, @@ -18,12 +18,12 @@ const PREVIEW_EDITOR_COLUMN_TYPES = [ ]; const EditorContainer = (props) => { - const { column } = props; + const { column, openEditorMode } = props; if (!column) return null; const { type } = column; if (POPUP_EDITOR_COLUMN_TYPES.includes(type)) { return ; - } else if (PREVIEW_EDITOR_COLUMN_TYPES.includes(type)) { + } else if (PREVIEW_EDITOR_COLUMN_TYPES.includes(type) && openEditorMode === EDITOR_TYPE.PREVIEWER) { return ; } else { return ; @@ -32,6 +32,7 @@ const EditorContainer = (props) => { EditorContainer.propTypes = { column: PropTypes.object, + openEditorMode: PropTypes.string, }; export default EditorContainer; diff --git a/frontend/src/metadata/components/cell-editors/editor-container/preview-editor-container.js b/frontend/src/metadata/components/cell-editors/editor-container/preview-editor-container.js index 96b2ce7d36d..4a9378076a7 100644 --- a/frontend/src/metadata/components/cell-editors/editor-container/preview-editor-container.js +++ b/frontend/src/metadata/components/cell-editors/editor-container/preview-editor-container.js @@ -2,7 +2,7 @@ import React from 'react'; import Editor from '../editor'; const PreviewEditorContainer = (props) => { - return (); + return (); }; export default PreviewEditorContainer; diff --git a/frontend/src/metadata/components/cell-editors/file-name-editor.js b/frontend/src/metadata/components/cell-editors/file-name-editor.js index 211e37acc76..97f9cb013d3 100644 --- a/frontend/src/metadata/components/cell-editors/file-name-editor.js +++ b/frontend/src/metadata/components/cell-editors/file-name-editor.js @@ -1,62 +1,29 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useImperativeHandle, useRef } from 'react'; import PropTypes from 'prop-types'; -import { ModalPortal } from '@seafile/sf-metadata-ui-component'; import { Utils } from '../../../utils/utils'; -import ImageDialog from '../../../components/dialog/image-dialog'; -import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants'; -import { PRIVATE_COLUMN_KEY } from '../../constants'; -import imageAPI from '../../../utils/image-api'; -import { seafileAPI } from '../../../utils/seafile-api'; -import toaster from '../../../components/toast'; - -const FileNameEditor = ({ column, record, table, onCommitCancel }) => { - const [imageIndex, setImageIndex] = useState(0); - const [imageItems, setImageItems] = useState([]); - - useEffect(() => { - const repoID = window.sfMetadataContext.getSetting('repoID'); - const repoInfo = window.sfMetadataContext.getSetting('repoInfo'); - const newImageItems = table.rows - .filter(row => Utils.imageCheck(row[PRIVATE_COLUMN_KEY.FILE_NAME])) - .map(item => { - const fileName = item[PRIVATE_COLUMN_KEY.FILE_NAME]; - const parentDir = item[PRIVATE_COLUMN_KEY.PARENT_DIR]; - const path = Utils.encodePath(Utils.joinPath(parentDir, fileName)); - const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase(); - const isGIF = fileExt === 'gif'; - const useThumbnail = repoInfo?.encrypted; - const basePath = `${siteRoot}${useThumbnail && !isGIF ? 'thumbnail' : 'repo'}/${repoID}`; - const src = `${basePath}/${useThumbnail && !isGIF ? thumbnailSizeForOriginal : 'raw'}${path}`; - return { - name: fileName, - url: `${siteRoot}lib/${repoID}/file${path}`, - thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`, - src: src, - downloadURL: `${fileServerRoot}repos/${repoID}/files${path}/?op=download`, - }; - }); - setImageItems(newImageItems); - }, [table]); +import { EDITOR_TYPE } from '../../constants'; +import ImagePreviewer from '../cell-formatter/image-previewer'; +import TextEditor from './text-editor'; +import { checkIsDir } from '../../utils/row'; + +const FileNameEditor = React.forwardRef((props, ref) => { + const { column, record, mode } = props; + const textEditorRef = useRef(null); + + useImperativeHandle(ref, () => { + return textEditorRef.current; + }); + + const getFileName = () => { + const { key } = column; + return record[key]; + }; - useEffect(() => { - if (imageItems.length > 0) { - const index = imageItems.findIndex(item => item.name === record[PRIVATE_COLUMN_KEY.FILE_NAME]); - if (index > -1) setImageIndex(index); + const getFileType = () => { + if (checkIsDir(record)) { + return 'folder'; } - }, [imageItems, record]); - - const _isDir = useMemo(() => { - const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR]; - if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE'; - return isDirValue; - }, [record]); - - const fileName = useMemo(() => { - return record[column.key]; - }, [column, record]); - - const fileType = useMemo(() => { - if (_isDir) return 'folder'; + const fileName = getFileName(); if (!fileName) return ''; const index = fileName.lastIndexOf('.'); if (index === -1) return ''; @@ -66,63 +33,27 @@ const FileNameEditor = ({ column, record, table, onCommitCancel }) => { if (Utils.isMarkdownFile(fileName)) return 'markdown'; if (Utils.isSdocFile(fileName)) return 'sdoc'; return ''; - }, [_isDir, fileName]); - - const moveToPrevImage = () => { - const imageItemsLength = imageItems.length; - setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength); - }; - - const moveToNextImage = () => { - const imageItemsLength = imageItems.length; - setImageIndex((prevState) => (prevState + 1) % imageItemsLength); }; - const rotateImage = (imageIndex, angle) => { - if (imageIndex >= 0 && angle !== 0) { - const repoID = window.sfMetadataContext.getSetting('repoID'); - const imageItem = imageItems[imageIndex]; - const path = imageItem.url.slice(imageItem.url.indexOf('/file/') + 5); - imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => { - if (res.data?.success) { - seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => { - if (res.data?.encoded_thumbnail_src) { - const cacheBuster = new Date().getTime(); - const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`; - imageItems[imageIndex].src = newThumbnailSrc; - setImageItems(imageItems); - } - }).catch(error => { - toaster.danger(Utils.getErrorMsg(error)); - }); - } - }).catch(error => { - toaster.danger(Utils.getErrorMsg(error)); - }); + if (mode === EDITOR_TYPE.PREVIEWER) { + const fileType = getFileType(); + if (fileType === 'image') { + return ( + + ); } - }; - if (fileType === 'image') { - return ( - - - - ); + return null; } - return null; -}; + return (); +}); FileNameEditor.propTypes = { + table: PropTypes.object, column: PropTypes.object, record: PropTypes.object, + mode: PropTypes.string, onCommitCancel: PropTypes.func, }; diff --git a/frontend/src/metadata/components/cell-formatter/image-previewer.js b/frontend/src/metadata/components/cell-formatter/image-previewer.js new file mode 100644 index 00000000000..5ef47c84270 --- /dev/null +++ b/frontend/src/metadata/components/cell-formatter/image-previewer.js @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { ModalPortal } from '@seafile/sf-metadata-ui-component'; +import toaster from '../../../components/toast'; +import ImageDialog from '../../../components/dialog/image-dialog'; +import imageAPI from '../../../utils/image-api'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants'; +import { getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell'; + +const ImagePreviewer = (props) => { + const { record, table, closeImagePopup } = props; + const [imageIndex, setImageIndex] = useState(0); + const [imageItems, setImageItems] = useState([]); + + useEffect(() => { + const repoID = window.sfMetadataContext.getSetting('repoID'); + const repoInfo = window.sfMetadataContext.getSetting('repoInfo'); + const newImageItems = table.rows + .filter((row) => Utils.imageCheck(getFileNameFromRecord(row))) + .map((row) => { + const fileName = getFileNameFromRecord(row); + const parentDir = getParentDirFromRecord(row); + const path = Utils.encodePath(Utils.joinPath(parentDir, fileName)); + const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase(); + const isGIF = fileExt === 'gif'; + const useThumbnail = repoInfo?.encrypted; + const basePath = `${siteRoot}${useThumbnail && !isGIF ? 'thumbnail' : 'repo'}/${repoID}`; + const src = `${basePath}/${useThumbnail && !isGIF ? thumbnailSizeForOriginal : 'raw'}${path}`; + return { + name: fileName, + url: `${siteRoot}lib/${repoID}/file${path}`, + thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`, + src: src, + downloadURL: `${fileServerRoot}repos/${repoID}/files${path}/?op=download`, + }; + }); + setImageItems(newImageItems); + }, [table]); + + useEffect(() => { + if (imageItems.length > 0) { + const index = imageItems.findIndex(item => item.name === getFileNameFromRecord(record)); + if (index > -1) setImageIndex(index); + } + }, [imageItems, record]); + + const moveToPrevImage = () => { + const imageItemsLength = imageItems.length; + setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength); + }; + + const moveToNextImage = () => { + const imageItemsLength = imageItems.length; + setImageIndex((prevState) => (prevState + 1) % imageItemsLength); + }; + + const rotateImage = (imageIndex, angle) => { + if (imageIndex >= 0 && angle !== 0) { + const repoID = window.sfMetadataContext.getSetting('repoID'); + const imageItem = imageItems[imageIndex]; + const path = imageItem.url.slice(imageItem.url.indexOf('/file/') + 5); + imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => { + if (res.data?.success) { + seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => { + if (res.data?.encoded_thumbnail_src) { + const cacheBuster = new Date().getTime(); + const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`; + imageItems[imageIndex].src = newThumbnailSrc; + setImageItems(imageItems); + } + }).catch(error => { + toaster.danger(Utils.getErrorMsg(error)); + }); + } + }).catch(error => { + toaster.danger(Utils.getErrorMsg(error)); + }); + } + }; + + return ( + + + + ); +}; + +ImagePreviewer.propTypes = { + table: PropTypes.object, + column: PropTypes.object, + record: PropTypes.object, + closeImagePopup: PropTypes.func, +}; + +export default ImagePreviewer; diff --git a/frontend/src/metadata/components/metadata-details/index.js b/frontend/src/metadata/components/metadata-details/index.js index 2a08a1b07ab..0451a00e3f0 100644 --- a/frontend/src/metadata/components/metadata-details/index.js +++ b/frontend/src/metadata/components/metadata-details/index.js @@ -7,7 +7,7 @@ import DetailItem from '../../../components/dirent-detail/detail-item'; import { Utils } from '../../../utils/utils'; import metadataAPI from '../../api'; import Column from '../../model/metadata/column'; -import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById } from '../../utils/cell'; +import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getFileNameFromRecord } from '../../utils/cell'; import { normalizeFields } from './utils'; import { gettext } from '../../../utils/constants'; import { CellType, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants'; @@ -102,7 +102,7 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType }) => { if (isLoading) return null; const { fields, record } = metadata; if (!record._id) return null; - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; + const fileName = getFileNameFromRecord(record); const isImage = record && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName)); return ( <> diff --git a/frontend/src/metadata/context.js b/frontend/src/metadata/context.js index 71fecae4937..98f692d0eb4 100644 --- a/frontend/src/metadata/context.js +++ b/frontend/src/metadata/context.js @@ -104,6 +104,11 @@ class Context { return true; }; + checkCanDeleteRow = () => { + if (this.permission === 'r') return false; + return true; + }; + canModifyRows = () => { if (this.permission === 'r') return false; return true; @@ -189,6 +194,10 @@ class Context { return this.metadataAPI.modifyRecords(repoId, recordsData, isCopyPaste); }; + batchDeleteFiles = (repoId, fileNames) => { + return this.metadataAPI.batchDeleteFiles(repoId, fileNames); + }; + // view modifyView = (repoId, viewId, viewData) => { return this.metadataAPI.modifyView(repoId, viewId, viewData); diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index a504f252be0..f5aa31e08f6 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -5,7 +5,8 @@ import Context from '../context'; import Store from '../store'; import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../constants'; import { Utils } from '../../utils/utils'; -import { useCollaborators, useMetadata } from '.'; +import { useMetadata } from './metadata'; +import { useCollaborators } from './collaborators'; import { gettext } from '../../utils/constants'; const MetadataViewContext = React.createContext(null); @@ -116,7 +117,15 @@ export const MetadataViewProvider = ({ }, [repoID, viewID]); return ( - + {children} ); @@ -127,6 +136,5 @@ export const useMetadataView = () => { if (!context) { throw new Error('\'MetadataContext\' is null'); } - const { isLoading, metadata, store } = context; - return { isLoading, metadata, store }; + return context; }; diff --git a/frontend/src/metadata/store/data-processor.js b/frontend/src/metadata/store/data-processor.js index d84d9d7fcc6..2d2c99801c7 100644 --- a/frontend/src/metadata/store/data-processor.js +++ b/frontend/src/metadata/store/data-processor.js @@ -109,6 +109,19 @@ class DataProcessor { // todo update sort and filter and ui change } + static updatePageDataWithDeleteRecords(deletedRowsIds, table) { + const { available_columns, groupbys, groups, rows } = table.view; + const idNeedDeletedMap = deletedRowsIds.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); + table.view.rows = rows.filter(rowId => !idNeedDeletedMap[rowId]); + + // remove record from group view + const _isGroupView = isGroupView({ groupbys }, available_columns); + if (_isGroupView) { + this.deleteGroupRows(groups, idNeedDeletedMap); + table.view.groups = this.deleteEmptyGroups(groups); + } + } + static handleReloadedRecords(table, reloadedRecords, relatedColumnKeyMap) { const idReloadedRecordMap = reloadedRecords.reduce((map, record) => { map[record._id] = record; @@ -175,7 +188,6 @@ class DataProcessor { static syncOperationOnData(table, operation, { collaborators }) { switch (operation.op_type) { - case OPERATION_TYPE.MODIFY_RECORD: case OPERATION_TYPE.MODIFY_RECORDS: { const { available_columns } = table.view; const { id_original_row_updates, row_ids } = operation; @@ -213,6 +225,12 @@ class DataProcessor { this.updateSummaries(); break; } + case OPERATION_TYPE.DELETE_RECORDS: { + const { rows_ids } = operation; + this.updatePageDataWithDeleteRecords(rows_ids, table); + this.updateSummaries(); + break; + } case OPERATION_TYPE.RESTORE_RECORDS: { const { rows_data, upper_row_ids } = operation; const { rows } = table.view; diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index 40cbf29f1a3..8703d5524d1 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -5,10 +5,13 @@ import { Operation, LOCAL_APPLY_OPERATION_TYPE, NEED_APPLY_AFTER_SERVER_OPERATION, OPERATION_TYPE, UNDO_OPERATION_TYPE, VIEW_OPERATION, COLUMN_OPERATION } from './operations'; -import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../constants'; +import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY } from '../constants'; import DataProcessor from './data-processor'; import ServerOperator from './server-operator'; import Metadata from '../model/metadata'; +import { checkIsDir } from '../utils/row'; +import { Utils } from '../../utils/utils'; +import { getFileNameFromRecord } from '../utils/cell'; class Store { @@ -140,13 +143,13 @@ class Store { return; } const operation = this.pendingOperations.shift(); - this.serverOperator.applyOperation(operation, this.sendOperationCallback.bind(this, undoRedoHandler)); + this.serverOperator.applyOperation(operation, this.data, this.sendOperationCallback.bind(this, undoRedoHandler)); } sendOperationCallback = (undoRedoHandler, { operation, error }) => { if (error) { - operation && operation.fail_callback && operation.fail_callback(); this.context.eventBus.dispatch(EVENT_BUS_TYPE.TABLE_ERROR, { error }); + operation && operation.fail_callback && operation.fail_callback(error); this.sendNextOperation(undoRedoHandler); return; } @@ -160,8 +163,8 @@ class Store { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.VIEW_CHANGED, this.data.view); } - operation.success_callback && operation.success_callback(); this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED); + operation.success_callback && operation.success_callback(); // need reload records if has related formula columns this.serverOperator.handleReloadRecords(this.data, operation, ({ reloadedRecords, idRecordNotExistMap, relatedColumnKeyMap }) => { @@ -230,30 +233,7 @@ class Store { DataProcessor.syncOperationOnData(this.data, operation, { collaborators: this.collaborators }); } - /** - * @param {String} row_id target row id - * @param {Object} updates { [column.name]: cell_value } - * @param {Object} original_updates { [column.key]: cell_value } - * @param {Object} old_row_data { [column.name]: cell_value } - * @param {Object} original_old_row_data { [column.key]: cell_value } - */ - modifyRecord(row_id, updates, old_row_data, original_updates, original_old_row_data) { - const row = getRowById(this.data, row_id); - if (!row || !this.context.canModifyRow(row)) return; - const type = OPERATION_TYPE.MODIFY_RECORD; - const operation = this.createOperation({ - type, - repo_id: this.repoId, - row_id, - updates, - old_row_data, - original_updates, - original_old_row_data, - }); - this.applyOperation(operation); - } - - modifyRecords(row_ids, id_row_updates, id_original_row_updates, id_old_row_data, id_original_old_row_data, is_copy_paste) { + modifyRecords(row_ids, id_row_updates, id_original_row_updates, id_old_row_data, id_original_old_row_data, is_copy_paste, is_rename, { fail_callback, success_callback }) { const originalRows = getRowsByIds(this.data, row_ids); let valid_row_ids = []; let valid_id_row_updates = {}; @@ -262,18 +242,48 @@ class Store { let valid_id_original_old_row_data = {}; let id_obj_id = {}; originalRows.forEach(row => { - if (!row || !this.context.canModifyRow(row)) { - return; + if (row && this.context.canModifyRow(row)) { + const rowId = row._id; + valid_row_ids.push(rowId); + id_obj_id[rowId] = row._obj_id; + valid_id_row_updates[rowId] = id_row_updates[rowId]; + valid_id_original_row_updates[rowId] = id_original_row_updates[rowId]; + valid_id_old_row_data[rowId] = id_old_row_data[rowId]; + valid_id_original_old_row_data[rowId] = id_original_old_row_data[rowId]; } - const rowId = row._id; - valid_row_ids.push(rowId); - valid_id_row_updates[rowId] = id_row_updates[rowId]; - id_obj_id[rowId] = row._obj_id; - valid_id_original_row_updates[rowId] = id_original_row_updates[rowId]; - valid_id_old_row_data[rowId] = id_old_row_data[rowId]; - valid_id_original_old_row_data[rowId] = id_original_old_row_data[rowId]; }); + // get updates which the parent dir is changed + let oldParentDirPath = null; + let newParentDirPath = null; + if (is_rename) { + const rowId = valid_row_ids[0]; + const row = getRowById(this.data, rowId); + if (row && checkIsDir(row)) { + const rowUpdates = id_original_row_updates[rowId]; + const oldName = getFileNameFromRecord(row); + const newName = getFileNameFromRecord(rowUpdates); + const { _parent_dir } = row; + oldParentDirPath = Utils.joinPath(_parent_dir, oldName); + newParentDirPath = Utils.joinPath(_parent_dir, newName); + } + + if (newParentDirPath) { + this.data.rows.forEach((row) => { + const { _id: rowId, _parent_dir: currentParentDir } = row; + if (currentParentDir.includes(oldParentDirPath) && !valid_row_ids.includes(rowId)) { + valid_row_ids.push(rowId); + id_obj_id[rowId] = row._obj_id; + const updates = { _parent_dir: currentParentDir.replace(oldParentDirPath, newParentDirPath) }; + valid_id_row_updates[rowId] = Object.assign({}, valid_id_row_updates[rowId], updates); + valid_id_original_row_updates[rowId] = Object.assign({}, valid_id_original_row_updates[rowId], updates); + valid_id_old_row_data[rowId] = Object.assign({}, valid_id_old_row_data[rowId], { _parent_dir: currentParentDir }); + valid_id_original_old_row_data[rowId] = Object.assign({}, valid_id_original_old_row_data[rowId], { _parent_dir: currentParentDir }); + } + }); + } + } + const type = OPERATION_TYPE.MODIFY_RECORDS; const operation = this.createOperation({ type, @@ -284,7 +294,53 @@ class Store { id_old_row_data: valid_id_old_row_data, id_original_old_row_data: valid_id_original_old_row_data, is_copy_paste, - id_obj_id: id_obj_id + is_rename, + id_obj_id: id_obj_id, + fail_callback, + success_callback, + }); + this.applyOperation(operation); + } + + deleteRecords(rows_ids, { fail_callback, success_callback }) { + const type = OPERATION_TYPE.DELETE_RECORDS; + + if (!Array.isArray(rows_ids) || rows_ids.length === 0) { + return; + } + const valid_rows_ids = Array.isArray(rows_ids) ? rows_ids.filter((rowId) => { + const row = getRowById(this.data, rowId); + return row && this.context.canModifyRow(row); + }) : []; + + + // delete rows where parent dir is deleted + const deletedDirsPaths = rows_ids.map((rowId) => { + const row = getRowById(this.data, rowId); + if (row && checkIsDir(row)) { + const { _parent_dir, _name } = row; + return Utils.joinPath(_parent_dir, _name); + } + return null; + }).filter(Boolean); + if (deletedDirsPaths.length > 0) { + this.data.rows.forEach((row) => { + if (deletedDirsPaths.some((deletedDirPath) => row._parent_dir.includes(deletedDirPath)) && !valid_rows_ids.includes(row._id)) { + valid_rows_ids.push(row._id); + } + }); + } + + if (valid_rows_ids.length === 0) { + return; + } + + const operation = this.createOperation({ + type, + repo_id: this.repoId, + rows_ids: valid_rows_ids, + fail_callback, + success_callback, }); this.applyOperation(operation); } @@ -443,6 +499,24 @@ class Store { this.applyOperation(operation); }; + checkIsRenameFileOperator = (rows_ids, id_original_row_updates) => { + if (rows_ids.length > 1) { + return false; + } + const rowId = rows_ids[0]; + const rowUpdates = id_original_row_updates[rowId]; + const updatedKeys = rowUpdates && Object.keys(rowUpdates); + if (!updatedKeys || updatedKeys.length > 1 || updatedKeys[0] !== PRIVATE_COLUMN_KEY.FILE_NAME) { + return false; + } + return true; + }; + + checkDuplicatedName = (name, parentDir) => { + const newPath = Utils.joinPath(parentDir, name); + return this.data.rows.some((row) => newPath === Utils.joinPath(row._parent_dir, row._name)); + }; + } export default Store; diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js index 611ae2825a5..9bd97bc0a23 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -12,46 +12,42 @@ export default function apply(data, operation) { const { op_type } = operation; switch (op_type) { - - case OPERATION_TYPE.MODIFY_RECORD: { - const { row_id, original_updates } = operation; - const { rows } = data; - const updatedRowIndex = rows.findIndex(row => row_id === row._id); - if (updatedRowIndex < 0) { - return data; - } - const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); - const modifier = window.sfMetadataContext.getUsername(); - const updatedRow = Object.assign({}, - rows[updatedRowIndex], - original_updates, - { '_mtime': modifyTime, '_last_modifier': modifier }, - ); - data.rows[updatedRowIndex] = updatedRow; - data.id_row_map[row_id] = updatedRow; - return data; - } case OPERATION_TYPE.MODIFY_RECORDS: { const { id_original_row_updates, id_row_updates } = operation; const { rows } = data; const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); const modifier = window.sfMetadataContext.getUsername(); let updatedRows = [...rows]; + rows.forEach((row, index) => { - const rowId = row._id; + const { _id: rowId } = row; const originalRowUpdates = id_original_row_updates[rowId]; const rowUpdates = id_row_updates[rowId]; - if (!rowUpdates && !originalRowUpdates) return; - const updatedRow = Object.assign({}, row, rowUpdates, originalRowUpdates, { - '_mtime': modifyTime, - '_last_modifier': modifier, - }); - updatedRows[index] = updatedRow; - data.id_row_map[rowId] = updatedRow; + if (rowUpdates || originalRowUpdates) { + const updatedRow = Object.assign({}, row, rowUpdates, originalRowUpdates, { + '_mtime': modifyTime, + '_last_modifier': modifier, + }); + updatedRows[index] = updatedRow; + data.id_row_map[rowId] = updatedRow; + } }); + data.rows = updatedRows; return data; } + case OPERATION_TYPE.DELETE_RECORDS: { + const { rows_ids } = operation; + const idNeedDeletedMap = rows_ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); + data.rows = data.rows.filter((row) => !idNeedDeletedMap[row._id]); + + // delete rows in id_row_map + rows_ids.forEach(rowId => { + delete data.id_row_map[rowId]; + }); + + return data; + } case OPERATION_TYPE.RESTORE_RECORDS: { const { original_rows } = operation; const currentTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js index 9d2f0ef3b48..758fb74c1c2 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -1,6 +1,6 @@ export const OPERATION_TYPE = { - MODIFY_RECORD: 'modify_record', MODIFY_RECORDS: 'modify_records', + DELETE_RECORDS: 'delete_records', RESTORE_RECORDS: 'restore_records', RELOAD_RECORDS: 'reload_records', MODIFY_FILTERS: 'modify_filters', @@ -28,8 +28,8 @@ export const COLUMN_DATA_OPERATION_TYPE = { }; export const OPERATION_ATTRIBUTES = { - [OPERATION_TYPE.MODIFY_RECORD]: ['repo_id', 'row_id', 'updates', 'old_row_data', 'original_updates', 'original_old_row_data'], - [OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'id_obj_id'], + [OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'], + [OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'rows_ids'], [OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'], [OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'], [OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters', 'basic_filters'], @@ -47,7 +47,6 @@ export const OPERATION_ATTRIBUTES = { }; export const UNDO_OPERATION_TYPE = [ - // OPERATION_TYPE.MODIFY_RECORD, // OPERATION_TYPE.MODIFY_RECORDS, // OPERATION_TYPE.RESTORE_RECORDS, // OPERATION_TYPE.INSERT_COLUMN, @@ -60,8 +59,8 @@ export const LOCAL_APPLY_OPERATION_TYPE = [ // apply operation after exec operation on the server export const NEED_APPLY_AFTER_SERVER_OPERATION = [ - OPERATION_TYPE.MODIFY_RECORD, OPERATION_TYPE.MODIFY_RECORDS, + OPERATION_TYPE.DELETE_RECORDS, OPERATION_TYPE.MODIFY_FILTERS, OPERATION_TYPE.MODIFY_SORTS, OPERATION_TYPE.MODIFY_GROUPBYS, diff --git a/frontend/src/metadata/store/operations/invert.js b/frontend/src/metadata/store/operations/invert.js index b0000d1f574..7152d2affc1 100644 --- a/frontend/src/metadata/store/operations/invert.js +++ b/frontend/src/metadata/store/operations/invert.js @@ -9,18 +9,6 @@ function createOperation(op) { export default function invert(operation) { const { op_type } = operation.clone(); switch (op_type) { - case OPERATION_TYPE.MODIFY_RECORD: { - const { page_id, row_id, updates, old_row_data, original_updates, original_old_row_data } = operation; - return createOperation({ - type: OPERATION_TYPE.MODIFY_RECORD, - page_id, - row_id, - updates: deepCopy(old_row_data), - old_row_data: deepCopy(updates), - original_updates: deepCopy(original_old_row_data), - original_old_row_data: deepCopy(original_updates), - }); - } case OPERATION_TYPE.MODIFY_RECORDS: { const { page_id, is_copy_paste, row_ids, id_row_updates, id_original_row_updates, diff --git a/frontend/src/metadata/store/server-operator.js b/frontend/src/metadata/store/server-operator.js index 467260f0fe5..835720932b4 100644 --- a/frontend/src/metadata/store/server-operator.js +++ b/frontend/src/metadata/store/server-operator.js @@ -1,27 +1,36 @@ +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; import { OPERATION_TYPE } from './operations'; import { getColumnByKey } from '../utils/column'; -import { gettext } from '../../utils/constants'; +import { getRowById } from '../utils/table'; +import { checkIsDir } from '../utils/row'; +import { getFileNameFromRecord } from '../utils/cell'; const MAX_LOAD_RECORDS = 100; class ServerOperator { - applyOperation(operation, callback) { + applyOperation(operation, data, callback) { const { op_type } = operation; switch (op_type) { - case OPERATION_TYPE.MODIFY_RECORD: { - const { repo_id, row_id, updates } = operation; - const recordsData = [{ record_id: row_id, record: updates }]; - window.sfMetadataContext.modifyRecords(repo_id, recordsData).then(res => { - callback({ operation }); - }).catch(error => { - callback({ error: gettext('Failed to modify record') }); - }); - break; - } case OPERATION_TYPE.MODIFY_RECORDS: { - const { repo_id, row_ids, id_row_updates, is_copy_paste, id_obj_id } = operation; + const { repo_id, row_ids, id_row_updates, id_original_row_updates, is_copy_paste, is_rename, id_obj_id } = operation; + if (is_rename) { + const rowId = row_ids[0]; + const rowUpdates = id_original_row_updates[rowId]; + const newName = getFileNameFromRecord(rowUpdates); + this.renameFile(newName, repo_id, rowId, data, { + fail_callback: (error) => { + callback({ error }); + }, + success_callback: () => { + callback({ operation }); + } + }); + return; + } const recordsData = row_ids.map(rowId => { return { record_id: rowId, record: id_row_updates[rowId], obj_id: id_obj_id[rowId] }; }); @@ -32,6 +41,23 @@ class ServerOperator { }); break; } + case OPERATION_TYPE.DELETE_RECORDS: { + const { repo_id, rows_ids } = operation; + const file_names = rows_ids.map((rowId) => { + const row = getRowById(data, rowId); + const { _parent_dir, _name } = row || {}; + if (_parent_dir && _name) { + return Utils.joinPath(_parent_dir, _name); + } + return null; + }).filter(Boolean); + window.sfMetadataContext.batchDeleteFiles(repo_id, file_names).then(res => { + callback({ operation }); + }).catch(error => { + callback({ error: gettext('Failed to delete records') }); + }); + break; + } case OPERATION_TYPE.RESTORE_RECORDS: { const { repo_id, rows_data } = operation; if (!Array.isArray(rows_data) || rows_data.length === 0) { @@ -307,6 +333,46 @@ class ServerOperator { return keys; }, []); } + + renameFile = (newName, repo_id, rowId, data, { fail_callback, success_callback }) => { + const row = getRowById(data, rowId); + if (!row) { + return; + } + + const { _parent_dir, _name } = row; + const path = Utils.joinPath(_parent_dir, _name); + + // rename folder + if (checkIsDir(row)) { + seafileAPI.renameDir(repo_id, path, newName).then(() => { + success_callback(); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + if (errMessage === gettext('Error')) { + errMessage = gettext('Renaming {name} failed').replace('{name}', _name); + } + fail_callback(errMessage); + }); + return; + } + + // rename file + seafileAPI.renameFile(repo_id, path, newName).then(() => { + success_callback(); + }).catch((error) => { + let errMessage = ''; + if (error && error.response.status == 403 && error.response.data && error.response.data['error_msg']) { + errMessage = error.response.data['error_msg']; + } else { + errMessage = Utils.getErrorMsg(error); + } + if (errMessage === gettext('Error')) { + errMessage = gettext('Renaming {name} failed').replace('{name}', _name); + } + fail_callback(errMessage); + }); + }; } export default ServerOperator; diff --git a/frontend/src/metadata/utils/cell/core.js b/frontend/src/metadata/utils/cell/core.js index 75206d628ff..ca25db65fc4 100644 --- a/frontend/src/metadata/utils/cell/core.js +++ b/frontend/src/metadata/utils/cell/core.js @@ -1,4 +1,4 @@ -import { PRIVATE_COLUMN_KEYS } from '../../constants'; +import { PRIVATE_COLUMN_KEY, PRIVATE_COLUMN_KEYS } from '../../constants'; /** * @param {any} value @@ -23,3 +23,11 @@ export const getCellValueByColumn = (record, column) => { if (PRIVATE_COLUMN_KEYS.includes(key)) return record[key]; return record[name]; }; + +export const getParentDirFromRecord = (record) => { + return record ? record[PRIVATE_COLUMN_KEY.PARENT_DIR] : ''; +}; + +export const getFileNameFromRecord = (record) => { + return record ? record[PRIVATE_COLUMN_KEY.FILE_NAME] : ''; +}; diff --git a/frontend/src/metadata/utils/cell/index.js b/frontend/src/metadata/utils/cell/index.js index e78ba0ba840..ac4a5b36f8e 100644 --- a/frontend/src/metadata/utils/cell/index.js +++ b/frontend/src/metadata/utils/cell/index.js @@ -1,6 +1,8 @@ export { isValidCellValue, getCellValueByColumn, + getParentDirFromRecord, + getFileNameFromRecord, } from './core'; export { diff --git a/frontend/src/metadata/utils/row/core.js b/frontend/src/metadata/utils/row/core.js index 530bd959d72..145cfcc471c 100644 --- a/frontend/src/metadata/utils/row/core.js +++ b/frontend/src/metadata/utils/row/core.js @@ -1,3 +1,4 @@ +import { PRIVATE_COLUMN_KEY } from '../../constants'; import { getTableById } from '../table'; /** @@ -25,6 +26,14 @@ const updateTableRowsWithRowsData = (tables, tableId, recordsData = []) => { }); }; +export const checkIsDir = (record) => { + const isDir = record[PRIVATE_COLUMN_KEY.IS_DIR]; + if (typeof isDir === 'string') { + return isDir.toUpperCase() === 'TRUE'; + } + return isDir; +}; + export { isTableRows, updateTableRowsWithRowsData, diff --git a/frontend/src/metadata/utils/row/index.js b/frontend/src/metadata/utils/row/index.js index cd8b4b64b9f..af843e8cd58 100644 --- a/frontend/src/metadata/utils/row/index.js +++ b/frontend/src/metadata/utils/row/index.js @@ -1,4 +1,5 @@ export { + checkIsDir, isTableRows, updateTableRowsWithRowsData, } from './core'; diff --git a/frontend/src/metadata/views/gallery/index.js b/frontend/src/metadata/views/gallery/index.js index f533d096003..36448ce9dcd 100644 --- a/frontend/src/metadata/views/gallery/index.js +++ b/frontend/src/metadata/views/gallery/index.js @@ -10,7 +10,7 @@ import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog'; import ModalPortal from '../../../components/modal-portal'; import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; -import { getDateDisplayString } from '../../utils/cell'; +import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell'; import { siteRoot, fileServerRoot, useGoFileserver, gettext, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants'; @@ -59,11 +59,11 @@ const Gallery = () => { const groups = useMemo(() => { if (isFirstLoading) return []; const firstSort = metadata.view.sorts[0]; - let init = metadata.rows.filter(row => Utils.imageCheck(row[PRIVATE_COLUMN_KEY.FILE_NAME])) + let init = metadata.rows.filter(row => Utils.imageCheck(getFileNameFromRecord(row))) .reduce((_init, record) => { const id = record[PRIVATE_COLUMN_KEY.ID]; - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + const fileName = getFileNameFromRecord(record); + const parentDir = getParentDirFromRecord(record); const path = Utils.encodePath(Utils.joinPath(parentDir, fileName)); const date = mode !== GALLERY_DATE_MODE.ALL ? getDateDisplayString(record[firstSort.column_key], dateMode) : ''; const img = { @@ -279,7 +279,7 @@ const Gallery = () => { const handleDelete = () => { if (selectedImages.length) { - metadataAPI.deleteImages(repoID, selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)) + metadataAPI.batchDeleteFiles(repoID, selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)) .then(() => { setSelectedImages([]); let msg = selectedImages.length > 1 diff --git a/frontend/src/metadata/views/table/context-menu/index.js b/frontend/src/metadata/views/table/context-menu/index.js index 037a6f59ce5..77b0b685c9e 100644 --- a/frontend/src/metadata/views/table/context-menu/index.js +++ b/frontend/src/metadata/views/table/context-menu/index.js @@ -1,12 +1,14 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import toaster from '../../../../components/toast'; -import { getColumnByKey } from '../../../utils/column'; import { gettext, siteRoot } from '../../../../utils/constants'; import { Utils } from '../../../../utils/utils'; import { useMetadataView } from '../../../hooks/metadata-view'; -import { PRIVATE_COLUMN_KEY } from '../../../constants'; -import { VIEW_TYPE } from '../../../constants/view'; +import { getColumnByKey, isNameColumn } from '../../../utils/column'; +import { checkIsDir } from '../../../utils/row'; +import { EVENT_BUS_TYPE, EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../../constants'; +import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell'; + import './index.css'; const OPERATION = { @@ -16,105 +18,144 @@ const OPERATION = { OPEN_IN_NEW_TAB: 'open-new-tab', GENERATE_DESCRIPTION: 'generate-description', IMAGE_CAPTION: 'image-caption', - DOWNLOAD: 'download', - DELETE: 'delete', + DELETE_RECORD: 'delete-record', + DELETE_RECORDS: 'delete-records', + RENAME_FILE: 'rename-file', }; -const ContextMenu = ({ - isGroupView, - selectedRange, - selectedPosition, - recordMetrics, - recordGetterByIndex, - onClearSelected, - onCopySelected, - updateRecords, - getTableContentRect, - getTableCanvasContainerRect, - onDownload, - onDelete, -}) => { +const ContextMenu = (props) => { + const { + isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected, updateRecords, + getTableContentRect, getTableCanvasContainerRect, deleteRecords, toggleDeleteFolderDialog, selectNone, + } = props; const menuRef = useRef(null); const [visible, setVisible] = useState(false); const [position, setPosition] = useState({ top: 0, left: 0 }); + const { metadata } = useMetadataView(); + const checkCanModifyRow = (row) => { + return window.sfMetadataContext.canModifyRow(row); + }; + + const checkIsDescribableDoc = useCallback((record) => { + const fileName = getFileNameFromRecord(record); + return checkCanModifyRow(record) && Utils.isDescriptionSupportedFile(fileName); + }, []); + + const getAbleDeleteRecords = useCallback((records) => { + return records.filter(record => window.sfMetadataContext.checkCanDeleteRow(record)); + }, []); + const options = useMemo(() => { if (!visible) return []; const permission = window.sfMetadataContext.getPermission(); const isReadonly = permission === 'r'; const { columns } = metadata; const descriptionColumn = getColumnByKey(columns, PRIVATE_COLUMN_KEY.FILE_DESCRIPTION); - const canModifyRow = window.sfMetadataContext.canModifyRow; let list = []; - if (metadata.view.type === VIEW_TYPE.GALLERY) { - list.push({ value: OPERATION.DOWNLOAD, label: gettext('Download') }); - list.push({ value: OPERATION.DELETE, label: gettext('Delete') }); - } - + // handle selected multiple cells if (selectedRange) { !isReadonly && list.push({ value: OPERATION.CLEAR_SELECTED, label: gettext('Clear selected') }); list.push({ value: OPERATION.COPY_SELECTED, label: gettext('Copy selected') }); + + const { topLeft, bottomRight } = selectedRange; + let records = []; + for (let i = topLeft.rowIdx; i <= bottomRight.rowIdx; i++) { + const record = recordGetterByIndex({ isGroupView, groupRecordIndex: topLeft.groupRecordIndex, recordIndex: i }); + if (record) { + records.push(record); + } + } + + const ableDeleteRecords = getAbleDeleteRecords(records); + if (ableDeleteRecords.length > 0) { + list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords }); + } + return list; } - const selectedRecords = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : []; - if (selectedRecords.length > 1) { + // handle selected records + const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : []; + if (selectedRecordsIds.length > 1) { + let records = []; + selectedRecordsIds.forEach(id => { + const record = metadata.id_row_map[id]; + if (record) { + records.push(record); + } + }); + + const ableDeleteRecords = getAbleDeleteRecords(records); + if (ableDeleteRecords.length > 0) { + list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords }); + } return list; } + // handle selected cell if (!selectedPosition) return list; - const { groupRecordIndex, rowIdx: recordIndex } = selectedPosition; + const { groupRecordIndex, rowIdx: recordIndex, idx } = selectedPosition; + const column = columns[idx]; const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); if (!record) return list; - const isFolder = record[PRIVATE_COLUMN_KEY.IS_DIR]; - list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab') }); - list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder') }); + + const canModifyRow = checkCanModifyRow(record); + const canDeleteRow = window.sfMetadataContext.checkCanDeleteRow(record); + const isFolder = checkIsDir(record); + list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab'), record }); + list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder'), record }); + if (descriptionColumn) { - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; - if (Utils.isDescriptionSupportedFile(fileName) && canModifyRow(record)) { - list.push({ value: OPERATION.GENERATE_DESCRIPTION, label: gettext('Generate description') }); - } else if (Utils.imageCheck(fileName) && canModifyRow(record)) { - list.push({ value: OPERATION.IMAGE_CAPTION, label: gettext('Generate image description') }); + if (checkIsDescribableDoc(record)) { + list.push({ value: OPERATION.GENERATE_DESCRIPTION, label: gettext('Generate description'), record }); + } else if (canModifyRow && Utils.imageCheck(getFileNameFromRecord(record))) { + list.push({ value: OPERATION.IMAGE_CAPTION, label: gettext('Generate image description'), record }); } } + // handle delete folder/file + if (canDeleteRow) { + list.push({ value: OPERATION.DELETE_RECORD, label: gettext('Delete'), record }); + } + + if (canModifyRow && column && isNameColumn(column)) { + list.push({ value: OPERATION.RENAME_FILE, label: gettext('Rename'), record }); + } + return list; - }, [visible, isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex]); + }, [visible, isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex, checkIsDescribableDoc, getAbleDeleteRecords]); const handleHide = useCallback((event) => { + if (!menuRef.current && visible) { + setVisible(false); + return; + } + if (menuRef.current && !menuRef.current.contains(event.target)) { setVisible(false); } - }, [menuRef]); + }, [menuRef, visible]); - const onOpenFileInNewTab = useCallback(() => { - const { groupRecordIndex, rowIdx } = selectedPosition; - const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx }); - if (!record) return; + const onOpenFileInNewTab = useCallback((record) => { const repoID = window.sfMetadataStore.repoId; - const isFolder = record[PRIVATE_COLUMN_KEY.IS_DIR]; - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; + const isFolder = checkIsDir(record); + const parentDir = getParentDirFromRecord(record); + const fileName = getFileNameFromRecord(record); - let url; - if (isFolder) { - url = window.location.origin + window.location.pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)); - } else { - url = `${siteRoot}lib/${repoID}/file${Utils.encodePath(Utils.joinPath(parentDir, fileName))}`; - } + const url = isFolder ? + window.location.origin + window.location.pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)) : + `${siteRoot}lib/${repoID}/file${Utils.encodePath(Utils.joinPath(parentDir, fileName))}`; window.open(url, '_blank'); - }, [isGroupView, recordGetterByIndex, selectedPosition]); + }, []); - const onOpenParentFolder = useCallback((event) => { + const onOpenParentFolder = useCallback((event, record) => { event.preventDefault(); event.stopPropagation(); - const { groupRecordIndex, rowIdx } = selectedPosition; - const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx }); - if (!record) return; - let parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + let parentDir = getParentDirFromRecord(record); if (window.location.pathname.endsWith('/')) { parentDir = parentDir.slice(1); @@ -122,19 +163,16 @@ const ContextMenu = ({ const url = window.location.origin + window.location.pathname + Utils.encodePath(parentDir); window.open(url, '_blank'); - }, [isGroupView, recordGetterByIndex, selectedPosition]); + }, []); - const generateDescription = useCallback(() => { - const canModifyRow = window.sfMetadataContext.canModifyRow; + const generateDescription = useCallback((record) => { const descriptionColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION; let path = ''; let idOldRecordData = {}; let idOriginalOldRecordData = {}; - const { groupRecordIndex, rowIdx } = selectedPosition; - const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx }); - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; - if (Utils.isDescriptionSupportedFile(fileName) && canModifyRow(record)) { - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + const fileName = getFileNameFromRecord(record); + if (Utils.isDescriptionSupportedFile(fileName) && checkCanModifyRow(record)) { + const parentDir = getParentDirFromRecord(record); path = Utils.joinPath(parentDir, fileName); idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [descriptionColumnKey]: record[descriptionColumnKey] }; idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [descriptionColumnKey]: record[descriptionColumnKey] }; @@ -153,19 +191,16 @@ const ContextMenu = ({ const errorMessage = gettext('Failed to generate description'); toaster.danger(errorMessage); }); - }, [isGroupView, selectedPosition, recordGetterByIndex, updateRecords]); + }, [updateRecords]); - const imageCaption = useCallback(() => { - const canModifyRow = window.sfMetadataContext.canModifyRow; + const imageCaption = useCallback((record) => { const summaryColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION; let path = ''; let idOldRecordData = {}; let idOriginalOldRecordData = {}; - const { groupRecordIndex, rowIdx } = selectedPosition; - const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx }); - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; - if (Utils.imageCheck(fileName) && canModifyRow(record)) { - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + const fileName = getFileNameFromRecord(record); + if (Utils.imageCheck(fileName) && checkCanModifyRow(record)) { + const parentDir = getParentDirFromRecord(record); path = Utils.joinPath(parentDir, fileName); idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] }; idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] }; @@ -184,17 +219,21 @@ const ContextMenu = ({ const errorMessage = gettext('Failed to generate image description'); toaster.danger(errorMessage); }); - }, [isGroupView, selectedPosition, recordGetterByIndex, updateRecords]); + }, [updateRecords]); const handleOptionClick = useCallback((event, option) => { event.stopPropagation(); switch (option.value) { case OPERATION.OPEN_IN_NEW_TAB: { - onOpenFileInNewTab(); + const { record } = option; + if (!record) break; + onOpenFileInNewTab(record); break; } case OPERATION.OPEN_PARENT_FOLDER: { - onOpenParentFolder(event); + const { record } = option; + if (!record) break; + onOpenParentFolder(event, record); break; } case OPERATION.COPY_SELECTED: { @@ -206,19 +245,43 @@ const ContextMenu = ({ break; } case OPERATION.GENERATE_DESCRIPTION: { - generateDescription && generateDescription(); + const { record } = option; + if (!record) break; + generateDescription(record); break; } case OPERATION.IMAGE_CAPTION: { - imageCaption && imageCaption(); + const { record } = option; + if (!record) break; + imageCaption(record); + break; + } + case OPERATION.DELETE_RECORD: { + const { record } = option; + if (!record || !record._id || !deleteRecords) break; + if (checkIsDir(record)) { + toggleDeleteFolderDialog(record); + break; + } + deleteRecords([record._id]); break; } - case OPERATION.DOWNLOAD: { - onDownload && onDownload(); + case OPERATION.DELETE_RECORDS: { + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + selectNone && selectNone(); + + const { records } = option; + const recordsIds = Array.isArray(records) ? records.map((record) => record._id).filter(Boolean) : []; + if (recordsIds.length === 0 || !deleteRecords) break; + deleteRecords(recordsIds); break; } - case OPERATION.DELETE: { - onDelete && onDelete(); + case OPERATION.RENAME_FILE: { + const { record } = option; + if (!record || !record._id) break; + + // rename file via FileNameEditor + window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR); break; } default: { @@ -226,7 +289,7 @@ const ContextMenu = ({ } } setVisible(false); - }, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, onDownload, onDelete]); + }, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, selectNone, deleteRecords, toggleDeleteFolderDialog]); const getMenuPosition = useCallback((x = 0, y = 0) => { let menuStyles = { @@ -272,7 +335,8 @@ const ContextMenu = ({ return () => { document.removeEventListener('contextmenu', handleShow); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -314,8 +378,10 @@ ContextMenu.propTypes = { selectedRange: PropTypes.object, selectedPosition: PropTypes.object, recordMetrics: PropTypes.object, + selectNone: PropTypes.func, getTableContentRect: PropTypes.func, recordGetterByIndex: PropTypes.func, + deleteRecords: PropTypes.func, }; export default ContextMenu; diff --git a/frontend/src/metadata/views/table/index.js b/frontend/src/metadata/views/table/index.js index c36058ec617..5a2b9f8a09f 100644 --- a/frontend/src/metadata/views/table/index.js +++ b/frontend/src/metadata/views/table/index.js @@ -2,8 +2,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import toaster from '../../../components/toast'; import TableMain from './table-main'; import { useMetadataView } from '../../hooks/metadata-view'; -import { Utils } from '../../../utils/utils'; +import { Utils, validateName } from '../../../utils/utils'; import { isModF } from '../../utils/hotkey'; +import { gettext } from '../../../utils/constants'; +import { getFileNameFromRecord } from '../../utils/cell'; import { getValidGroupbys } from '../../utils/group'; import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, MAX_LOAD_NUMBER } from '../../constants'; @@ -11,7 +13,7 @@ import './index.css'; const Table = () => { const [isLoadingMore, setLoadingMore] = useState(false); - const { isLoading, metadata, store } = useMetadataView(); + const { isLoading, metadata, store, renameFileCallback, deleteFilesCallback } = useMetadataView(); const containerRef = useRef(null); const onKeyDown = useCallback((event) => { @@ -67,18 +69,80 @@ const Table = () => { } }, [metadata, store]); - const modifyRecords = useCallback((rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste = false) => { - store.modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste); - }, [store]); + const modifyRecords = (rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste = false) => { + const isRename = store.checkIsRenameFileOperator(rowIds, idOriginalRowUpdates); + let oldPath = null; + let newName = null; + if (isRename) { + const rowId = rowIds[0]; + const row = recordGetterById(rowId); + const rowUpdates = idOriginalRowUpdates[rowId]; + const { _parent_dir, _name } = row; + oldPath = Utils.joinPath(_parent_dir, _name); + newName = getFileNameFromRecord(rowUpdates); + const { isValid, errMessage } = validateName(newName); + if (!isValid) { + toaster.danger(errMessage); + return; + } + if (newName === _name) { + return; + } + if (store.checkDuplicatedName(newName, _parent_dir)) { + let errMessage = gettext('The name "{name}" is already taken. Please choose a different name.'); + errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName)); + toaster.danger(errMessage); + return; + } + } + store.modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste, isRename, { + fail_callback: (error) => { + toaster.danger(error); + }, + success_callback: () => { + if (isRename) { + renameFileCallback(oldPath, newName); + } + }, + }); + }; + + const deleteRecords = (recordsIds) => { + let paths = []; + let fileNames = []; + recordsIds.forEach((recordId) => { + const record = recordGetterById(recordId); + const { _parent_dir, _name } = record || {}; + if (_parent_dir && _name) { + const path = Utils.joinPath(_parent_dir, _name); + paths.push(path); + fileNames.push(_name); + } + }); + store.deleteRecords(recordsIds, { + fail_callback: (error) => { + toaster.danger(error); + }, + success_callback: () => { + deleteFilesCallback(paths, fileNames); + let msg = fileNames.length > 1 + ? gettext('Successfully deleted {name} and {n} other items') + : gettext('Successfully deleted {name}'); + msg = msg.replace('{name}', fileNames[0]) + .replace('{n}', fileNames.length - 1); + toaster.success(msg); + }, + }); + }; - const modifyRecord = useCallback((rowId, updates, oldRowData, originalUpdates, originalOldRowData) => { + const modifyRecord = (rowId, updates, oldRowData, originalUpdates, originalOldRowData) => { const rowIds = [rowId]; const idRowUpdates = { [rowId]: updates }; const idOriginalRowUpdates = { [rowId]: originalUpdates }; const idOldRowData = { [rowId]: oldRowData }; const idOriginalOldRowData = { [rowId]: originalOldRowData }; modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData); - }, [modifyRecords]); + }; const getAdjacentRowsIds = useCallback((rowIds) => { const rowIdsLen = metadata.row_ids.length; @@ -162,6 +226,7 @@ const Table = () => { metadata={metadata} modifyRecord={modifyRecord} modifyRecords={modifyRecords} + deleteRecords={deleteRecords} recordGetterById={recordGetterById} recordGetterByIndex={recordGetterByIndex} getTableContentRect={getTableContentRect} diff --git a/frontend/src/metadata/views/table/masks/interaction-masks/index.js b/frontend/src/metadata/views/table/masks/interaction-masks/index.js index 6242fc39f49..c1f9cd04a9a 100644 --- a/frontend/src/metadata/views/table/masks/interaction-masks/index.js +++ b/frontend/src/metadata/views/table/masks/interaction-masks/index.js @@ -27,6 +27,7 @@ import RecordMetrics from '../../utils/record-metrics'; import setEventTransfer from '../../utils/set-event-transfer'; import getEventTransfer from '../../utils/get-event-transfer'; import { getGroupRecordByIndex } from '../../utils/group-metrics'; +import { isNameColumn } from '../../../../utils/column'; import './index.css'; @@ -219,6 +220,7 @@ class InteractionMasks extends React.Component { const { selectedPosition, openEditorMode } = this.state; const { columns } = this.props; const selectedColumn = getSelectedColumn({ selectedPosition, columns }); + const _isNameColumn = isNameColumn(selectedColumn); const { type: columnType } = selectedColumn; if (NOT_SUPPORT_OPEN_EDITOR_COLUMN_TYPES.includes(columnType)) return null; @@ -226,7 +228,7 @@ class InteractionMasks extends React.Component { // how to open editors? // 1. editor is closed // 2. record-cell is editable or open editor with preview mode - if (((this.isSelectedCellEditable() || (openEditorMode === EDITOR_TYPE.PREVIEWER && READONLY_PREVIEW_COLUMNS.includes(columnType))) && !this.state.isEditorEnabled)) { + if (((this.isSelectedCellEditable() || _isNameColumn || (openEditorMode === EDITOR_TYPE.PREVIEWER && READONLY_PREVIEW_COLUMNS.includes(columnType))) && !this.state.isEditorEnabled)) { this.setState({ isEditorEnabled: true, firstEditorKeyDown: key, diff --git a/frontend/src/metadata/views/table/table-main/index.js b/frontend/src/metadata/views/table/table-main/index.js index 9f20d5d1ccd..b13e3efa1aa 100644 --- a/frontend/src/metadata/views/table/table-main/index.js +++ b/frontend/src/metadata/views/table/table-main/index.js @@ -7,7 +7,7 @@ import { GROUP_VIEW_OFFSET } from '../../../constants'; import './index.css'; -const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, recordGetterByIndex, recordGetterById, modifyColumnData, ...params }) => { +const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, recordGetterByIndex, recordGetterById, modifyColumnData, ...props }) => { const gridUtils = useMemo(() => { return new GridUtils(metadata, { modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById, modifyColumnData }); @@ -60,11 +60,12 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s groupOffsetLeft={groupOffset} modifyRecord={updateRecord} updateRecords={updateRecords} + deleteRecords={props.deleteRecords} getCopiedRecordsAndColumnsFromRange={getCopiedRecordsAndColumnsFromRange} recordGetterById={recordGetterById} recordGetterByIndex={recordGetterByIndex} modifyColumnData={modifyColumnData} - {...params} + {...props} /> ); diff --git a/frontend/src/metadata/views/table/table-main/records-footer/index.js b/frontend/src/metadata/views/table/table-main/records-footer/index.js index 751ef852eee..9633b9511ba 100644 --- a/frontend/src/metadata/views/table/table-main/records-footer/index.js +++ b/frontend/src/metadata/views/table/table-main/records-footer/index.js @@ -120,8 +120,8 @@ class RecordsFooter extends React.Component { let recordsCountText; if (recordsCount > 1) { recordsCountText = gettext('xxx records').replace('xxx', recordsCount); - } else if (recordsCount === 1) { - recordsCountText = gettext('1 record'); + } else { + recordsCountText = gettext('xxx record').replace('xxx', recordsCount); } if (hasMore) { recordsCountText += ' +'; diff --git a/frontend/src/metadata/views/table/table-main/records/index.js b/frontend/src/metadata/views/table/table-main/records/index.js index a684120fc42..58520160dc4 100644 --- a/frontend/src/metadata/views/table/table-main/records/index.js +++ b/frontend/src/metadata/views/table/table-main/records/index.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { HorizontalScrollbar } from '../../../../components/scrollbar'; import EmptyTip from '../../../../../components/empty-tip'; +import DeleteFolderDialog from '../../../../../components/dialog/delete-folder-dialog'; import Body from './body'; import GroupBody from './group-body'; import RecordsHeader from '../records-header'; @@ -10,7 +11,7 @@ import ContextMenu from '../../context-menu'; import { recalculate } from '../../../../utils/column'; import { getEventClassName } from '../../../../utils/common'; import { SEQUENCE_COLUMN_WIDTH, CANVAS_RIGHT_INTERVAL, GROUP_ROW_TYPE, EVENT_BUS_TYPE } from '../../../../constants'; -import { isMobile } from '../../../../../utils/utils'; +import { isMobile, Utils } from '../../../../../utils/utils'; import { isShiftKeyDown } from '../../../../utils/keyboard-utils'; import { gettext } from '../../../../../utils/constants'; import RecordMetrics from '../../utils/record-metrics'; @@ -42,9 +43,11 @@ class Records extends Component { }, selectedPosition: this.initPosition, ...initHorizontalScrollState, + deletedFolderPath: '', }; this.isWindows = isWindowsBrowser(); this.isWebkit = isWebkitBrowser(); + this.deletedRecord = null; } componentDidMount() { @@ -614,15 +617,40 @@ class Records extends Component { return this.resultContainerRef.getBoundingClientRect(); }; + toggleDeleteFolderDialog = (record) => { + if (this.state.deletedFolderPath) { + this.deletedRecord = null; + this.setState({ deletedFolderPath: '' }); + } else { + const { _parent_dir, _name } = record; + const deletedFolderPath = Utils.joinPath(_parent_dir, _name); + this.deletedRecord = record; + this.setState({ deletedFolderPath: deletedFolderPath }); + } + }; + + deleteFolder = () => { + if (!this.deletedRecord) return; + this.props.deleteRecords([this.deletedRecord._id]); + }; + renderRecordsBody = ({ containerWidth }) => { - const { isGroupView, recordGetterByIndex, updateRecords } = this.props; + const { isGroupView } = this.props; const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state; const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics; const commonProps = { ...this.props, columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth, recordMetrics, colOverScanStartIdx, colOverScanEndIdx, - contextMenu: (), + contextMenu: ( + + ), hasSelectedRecord: this.hasSelectedRecord(), getScrollLeft: this.getScrollLeft, getScrollTop: this.getScrollTop, @@ -658,15 +686,17 @@ class Records extends Component { }; render() { - const { recordIds, recordsCount, table, isGroupView, groupOffsetLeft, renameColumn, modifyColumnData, - deleteColumn, modifyColumnOrder } = this.props; + const { + recordIds, recordsCount, table, isGroupView, groupOffsetLeft, renameColumn, modifyColumnData, + deleteColumn, modifyColumnOrder, + } = this.props; const { recordMetrics, columnMetrics, selectedRange, colOverScanStartIdx, colOverScanEndIdx } = this.state; const { columns, totalWidth, lastFrozenColumnKey } = columnMetrics; const containerWidth = totalWidth + SEQUENCE_COLUMN_WIDTH + CANVAS_RIGHT_INTERVAL + groupOffsetLeft; const hasSelectedRecord = this.hasSelectedRecord(); const isSelectedAll = RecordMetrics.isSelectedAll(recordIds, recordMetrics); - if (recordsCount === 0) { + if (recordsCount === 0 && !this.props.hasMore) { return (); } @@ -726,6 +756,14 @@ class Records extends Component { getRecordsSummaries={() => { }} loadAll={this.props.loadAll} /> + {this.state.deletedFolderPath && ( + + )} ); } diff --git a/frontend/src/metadata/views/table/table-main/records/record/cell/index.js b/frontend/src/metadata/views/table/table-main/records/record/cell/index.js index 14fc90c543d..d04e82b29eb 100644 --- a/frontend/src/metadata/views/table/table-main/records/record/cell/index.js +++ b/frontend/src/metadata/views/table/table-main/records/record/cell/index.js @@ -6,7 +6,8 @@ import CellOperationBtn from './operation-btn'; import { Utils } from '../../../../../../../utils/utils'; import ObjectUtils from '../../../../../../utils/object-utils'; import { isCellValueChanged, getCellValueByColumn } from '../../../../../../utils/cell'; -import { CellType, PRIVATE_COLUMN_KEY, PRIVATE_COLUMN_KEYS, TABLE_SUPPORT_EDIT_TYPE_MAP } from '../../../../../../constants'; +import { CellType, PRIVATE_COLUMN_KEYS, TABLE_SUPPORT_EDIT_TYPE_MAP } from '../../../../../../constants'; +import { checkIsDir } from '../../../../../../utils/row'; import './index.css'; @@ -41,9 +42,7 @@ const Cell = React.memo(({ return column.type === CellType.FILE_NAME; }, [column]); const isDir = useMemo(() => { - const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR]; - if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE'; - return isDirValue; + return checkIsDir(record); }, [record]); const style = useMemo(() => { const { left, width } = column; diff --git a/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/index.js b/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/index.js index edf88392945..d2b030adf33 100644 --- a/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/index.js +++ b/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/index.js @@ -5,7 +5,9 @@ import { IconBtn } from '@seafile/sf-metadata-ui-component'; import { Utils } from '../../../../../../../../utils/utils'; import { gettext, siteRoot } from '../../../../../../../../utils/constants'; import { EVENT_BUS_TYPE } from '../../../../../../../..//components/common/event-bus-type'; -import { EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, EDITOR_TYPE, PRIVATE_COLUMN_KEY } from '../../../../../../../constants'; +import { EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, EDITOR_TYPE } from '../../../../../../../constants'; +import { getFileNameFromRecord, getParentDirFromRecord } from '../../../../../../../utils/cell'; +import { checkIsDir } from '../../../../../../../utils/row'; import './index.css'; @@ -18,19 +20,13 @@ const FILE_TYPE = { const CellOperationBtn = ({ isDir, column, record, cellValue, ...props }) => { - const _isDir = useMemo(() => { - const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR]; - if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE'; - return isDirValue; - }, [record]); - const fileName = useMemo(() => { const { key } = column; return record[key]; }, [column, record]); const fileType = useMemo(() => { - if (_isDir) return FILE_TYPE.FOLDER; + if (checkIsDir(record)) return FILE_TYPE.FOLDER; if (!fileName) return ''; const index = fileName.lastIndexOf('.'); if (index === -1) return ''; @@ -40,10 +36,10 @@ const CellOperationBtn = ({ isDir, column, record, cellValue, ...props }) => { if (Utils.isMarkdownFile(fileName)) return FILE_TYPE.MARKDOWN; if (Utils.isSdocFile(fileName)) return FILE_TYPE.SDOC; return ''; - }, [_isDir, fileName]); + }, [record, fileName]); const getParentDir = () => { - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + const parentDir = getParentDirFromRecord(record); if (parentDir === '/') { return ''; } @@ -62,8 +58,8 @@ const CellOperationBtn = ({ isDir, column, record, cellValue, ...props }) => { }; const openMarkdown = () => { - const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME]; - const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + const fileName = getFileNameFromRecord(record); + const parentDir = getParentDirFromRecord(record); window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.OPEN_MARKDOWN_DIALOG, parentDir, fileName); }; diff --git a/frontend/src/metadata/views/table/utils/grid-utils.js b/frontend/src/metadata/views/table/utils/grid-utils.js index f0fe0eac57a..07d1edfe6f6 100644 --- a/frontend/src/metadata/views/table/utils/grid-utils.js +++ b/frontend/src/metadata/views/table/utils/grid-utils.js @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import { getCellValueByColumn } from '../../../utils/cell'; +import { getCellValueByColumn, getFileNameFromRecord } from '../../../utils/cell'; import { getColumnByIndex, getColumnOriginName } from '../../../utils/column'; import { CellType, NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES, PRIVATE_COLUMN_KEY, TRANSFER_TYPES } from '../../../constants'; import { getGroupRecordByIndex } from './group-metrics'; @@ -96,7 +96,7 @@ class GridUtils { let originalOldRecordData = {}; let originalKeyOldRecordData = {}; const { canModifyRow, canModifyColumn } = window.sfMetadataContext; - const filename = pasteRecord[PRIVATE_COLUMN_KEY.FILE_NAME]; + const filename = getFileNameFromRecord(pasteRecord); for (let j = 0; j < pasteColumnsLen; j++) { const pasteColumn = getColumnByIndex(j + startColumnIndex, columns); diff --git a/frontend/src/metadata/views/table/utils/selected-cell-utils.js b/frontend/src/metadata/views/table/utils/selected-cell-utils.js index 8c4f0172bc9..54ca2083707 100644 --- a/frontend/src/metadata/views/table/utils/selected-cell-utils.js +++ b/frontend/src/metadata/views/table/utils/selected-cell-utils.js @@ -1,5 +1,5 @@ import { Utils } from '../../../../utils/utils'; -import { getCellValueByColumn } from '../../../utils/cell'; +import { getCellValueByColumn, getFileNameFromRecord } from '../../../utils/cell'; import { getGroupByPath } from '../../../utils/view'; import { getColumnByIndex, canEditCell } from '../../../utils/column'; import { PRIVATE_COLUMN_KEY, SUPPORT_PREVIEW_COLUMN_TYPES, metadataZIndexes } from '../../../constants'; @@ -46,7 +46,7 @@ export const isSelectedCellEditable = ({ enableCellSelect, selectedPosition, col const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex }); if (!window.sfMetadataContext.canModifyRow(row)) return false; let isCellEditable = Utils.isFunction(onCheckCellIsEditable) ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true; - const fileName = row ? row[PRIVATE_COLUMN_KEY.FILE_NAME] : ''; + const fileName = getFileNameFromRecord(row); const imageRow = row && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName)); isCellEditable = isCellEditable && canEditCell(column, row, enableCellSelect); if (imageRow) return isCellEditable; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 3a76fd0c43e..57abea61385 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -932,13 +932,7 @@ class LibContentView extends React.Component { this.setState({ currentDirent: null }); seafileAPI.deleteMutipleDirents(repoID, this.state.path, dirNames).then(res => { - if (this.state.isTreePanelShown) { - this.deleteTreeNodes(direntPaths); - } - - this.deleteDirents(dirNames); - - this.removeFromRecentlyUsed(repoID, this.state.path); + this.deleteItemsAjaxCallback(direntPaths, dirNames); let msg = ''; if (direntPaths.length > 1) { @@ -1174,12 +1168,12 @@ class LibContentView extends React.Component { } }; - renameItemAjaxCallback(path, newName) { + renameItemAjaxCallback = (path, newName) => { if (this.state.isTreePanelShown) { this.renameTreeNode(path, newName); } this.renameDirent(path, newName); - } + }; toggleDeleteFolderDialog = () => { this.setState({ isDeleteFolderDialogOpen: !this.state.isDeleteFolderDialogOpen }); @@ -1246,6 +1240,14 @@ class LibContentView extends React.Component { this.removeFromRecentlyUsed(this.props.repoID, path); } + deleteItemsAjaxCallback = (direntPaths, dirNames) => { + if (this.state.isTreePanelShown) { + this.deleteTreeNodes(direntPaths); + } + this.deleteDirents(dirNames); + this.removeFromRecentlyUsed(this.props.repoID, this.state.path); + }; + // list operations onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath) => { this.updateCurrentDirent(dirent); @@ -2368,6 +2370,8 @@ class LibContentView extends React.Component { onItemSelected={this.onDirentSelected} onItemDelete={this.onMainPanelItemDelete} onItemRename={this.onMainPanelItemRename} + deleteFilesCallback={this.deleteItemsAjaxCallback} + renameFileCallback={this.renameItemAjaxCallback} onItemMove={this.onMoveItem} onItemCopy={this.onCopyItem} onItemConvert={this.onConvertItem}