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}