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 c98dc1f6ba..330485e27b 100644
--- a/frontend/src/components/dir-view-mode/dir-column-view.js
+++ b/frontend/src/components/dir-view-mode/dir-column-view.js
@@ -64,7 +64,9 @@ const propTypes = {
deleteFilesCallback: PropTypes.func,
renameFileCallback: PropTypes.func,
onItemMove: PropTypes.func.isRequired,
+ moveFileCallback: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
+ copyFileCallback: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onDirentClick: PropTypes.func.isRequired,
isAllItemSelected: PropTypes.bool.isRequired,
@@ -205,6 +207,9 @@ class DirColumnView extends React.Component {
viewID={this.props.viewId}
deleteFilesCallback={this.props.deleteFilesCallback}
renameFileCallback={this.props.renameFileCallback}
+ moveFileCallback={this.props.moveFileCallback}
+ copyFileCallback={this.props.copyFileCallback}
+ addFolder={this.props.onAddFolder}
updateCurrentDirent={this.props.updateCurrentDirent}
closeDirentDetail={this.props.closeDirentDetail}
showDirentDetail={this.props.showDirentDetail}
diff --git a/frontend/src/metadata/context.js b/frontend/src/metadata/context.js
index 6657b00b10..ba03f85045 100644
--- a/frontend/src/metadata/context.js
+++ b/frontend/src/metadata/context.js
@@ -125,6 +125,12 @@ class Context {
return true;
};
+ canDuplicateRow = () => {
+ if (this.permission === 'r') return false;
+ const viewId = this.getSetting('viewID');
+ return viewId !== FACE_RECOGNITION_VIEW_ID;
+ };
+
canModifyColumn = (column) => {
if (this.permission === 'r') return false;
const { editable } = column;
diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js
index 72b5dee703..74e3a51c61 100644
--- a/frontend/src/metadata/hooks/metadata-view.js
+++ b/frontend/src/metadata/hooks/metadata-view.js
@@ -3,13 +3,14 @@ import React, { useContext, useEffect, useRef, useState, useCallback } from 'rea
import toaster from '../../components/toast';
import Context from '../context';
import Store from '../store';
-import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../constants';
+import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY } from '../constants';
import { Utils, validateName } from '../../utils/utils';
import { useMetadata } from './metadata';
import { useCollaborators } from './collaborators';
import { getRowById } from '../utils/table';
-import { getFileNameFromRecord, getParentDirFromRecord } from '../utils/cell';
+import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord, getUniqueFileName } from '../utils/cell';
import { gettext } from '../../utils/constants';
+import { checkIsDir } from '../utils/row';
const MetadataViewContext = React.createContext(null);
@@ -19,12 +20,17 @@ export const MetadataViewProvider = ({
viewID,
renameFileCallback,
deleteFilesCallback,
+ moveFileCallback,
+ copyFileCallback,
...params
}) => {
const [isLoading, setLoading] = useState(true);
const [metadata, setMetadata] = useState({ rows: [], columns: [], view: {} });
const [errorMessage, setErrorMessage] = useState(null);
+
const storeRef = useRef(null);
+ const delayReloadDataTimer = useRef(null);
+
const { collaborators } = useCollaborators();
const { isBeingBuilt, setIsBeingBuilt } = useMetadata();
@@ -45,6 +51,7 @@ export const MetadataViewProvider = ({
storeRef.current.reload(PER_LOAD_NUMBER).then(() => {
setMetadata(storeRef.current.data);
setLoading(false);
+ delayReloadDataTimer.current = null;
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
setErrorMessage(errorMsg);
@@ -52,6 +59,13 @@ export const MetadataViewProvider = ({
});
}, []);
+ const delayReloadMetadata = useCallback(() => {
+ delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current);
+ delayReloadDataTimer.current = setTimeout(() => {
+ reloadMetadata();
+ }, 600);
+ }, [reloadMetadata]);
+
const modifyFilters = useCallback((filters, filterConjunction, basicFilters) => {
storeRef.current.modifyFilters(filterConjunction, filters, basicFilters);
}, [storeRef]);
@@ -162,6 +176,78 @@ export const MetadataViewProvider = ({
modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste, { success_callback, fail_callback });
};
+ const moveRecord = (rowId, targetRepo, dirent, targetParentPath, sourceParentPath, isByDialog) => {
+ const targetRepoId = targetRepo.repo_id;
+ const row = getRowById(metadata, rowId);
+ const { rows } = metadata;
+ const isDir = checkIsDir(row);
+ const oldName = dirent.name;
+ const oldParentPath = Utils.joinPath(sourceParentPath, oldName);
+
+ let needDeletedRowIds = [];
+ let updateRowIds = [];
+ let idRowUpdates = {};
+ let idOldRowData = {};
+
+ if (repoID === targetRepoId) {
+ const newName = getUniqueFileName(rows, targetParentPath, oldName);
+ updateRowIds.push(rowId);
+ idRowUpdates[rowId] = { [PRIVATE_COLUMN_KEY.PARENT_DIR]: targetParentPath, [PRIVATE_COLUMN_KEY.FILE_NAME]: newName };
+ idOldRowData[rowId] = { [PRIVATE_COLUMN_KEY.PARENT_DIR]: sourceParentPath, [PRIVATE_COLUMN_KEY.FILE_NAME]: oldName };
+ if (isDir) {
+ const newPath = Utils.joinPath(targetParentPath, newName);
+ rows.forEach((row) => {
+ const parentDir = getParentDirFromRecord(row);
+ if (row && parentDir.startsWith(oldParentPath)) {
+ const updateRowId = getRecordIdFromRecord(row);
+ updateRowIds.push(updateRowId);
+ idRowUpdates[updateRowId] = { [PRIVATE_COLUMN_KEY.PARENT_DIR]: parentDir.replace(oldParentPath, newPath) };
+ idOldRowData[updateRowId] = { [PRIVATE_COLUMN_KEY.PARENT_DIR]: parentDir };
+ }
+ });
+ }
+ } else {
+ needDeletedRowIds = [rowId];
+ if (isDir) {
+ rows.forEach((row) => {
+ const parentDir = getParentDirFromRecord(row);
+ if (row && parentDir.startsWith(oldParentPath)) {
+ const id = getRecordIdFromRecord(row);
+ needDeletedRowIds.push(id);
+ }
+ });
+ }
+ }
+
+ storeRef.current.moveRecord(rowId, targetRepoId, dirent, targetParentPath, sourceParentPath, {
+ modify_row_ids: updateRowIds,
+ modify_id_row_updates: idRowUpdates,
+ modify_id_old_row_data: idOldRowData,
+ delete_row_ids: needDeletedRowIds,
+ }, {
+ success_callback: (operation) => {
+ moveFileCallback && moveFileCallback(repoID, targetRepo, dirent, targetParentPath, sourceParentPath, operation.task_id, isByDialog);
+ },
+ fail_callback: (error) => {
+ error && toaster.danger(error);
+ }
+ });
+ };
+
+ const duplicateRecord = (rowId, targetRepo, dirent, targetPath, nodeParentPath, isByDialog) => {
+ storeRef.current.duplicateRecord(rowId, targetRepo.repo_id, dirent, targetPath, nodeParentPath, {
+ success_callback: (operation) => {
+ copyFileCallback && copyFileCallback(repoID, targetRepo, dirent, targetPath, nodeParentPath, operation.task_id, isByDialog);
+ if (repoID === targetRepo.repo_id) {
+ delayReloadMetadata();
+ }
+ },
+ fail_callback: (error) => {
+ error && toaster.danger(error);
+ }
+ });
+ };
+
const renameColumn = useCallback((columnKey, newName, oldName) => {
storeRef.current.renameColumn(columnKey, newName, oldName);
}, [storeRef]);
@@ -239,6 +325,7 @@ export const MetadataViewProvider = ({
unsubscribeModifyColumnOrder();
unsubscribeModifySettings();
unsubscribeLocalRecordChanged();
+ delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, viewID]);
@@ -261,6 +348,8 @@ export const MetadataViewProvider = ({
modifyRecords,
deleteRecords,
modifyRecord,
+ moveRecord,
+ duplicateRecord,
renameColumn,
deleteColumn,
modifyColumnOrder,
@@ -268,6 +357,7 @@ export const MetadataViewProvider = ({
modifyColumnWidth,
insertColumn,
updateFileTags,
+ addFolder: params.addFolder,
}}
>
{children}
diff --git a/frontend/src/metadata/store/data-processor.js b/frontend/src/metadata/store/data-processor.js
index 5838744d1b..9367e305a5 100644
--- a/frontend/src/metadata/store/data-processor.js
+++ b/frontend/src/metadata/store/data-processor.js
@@ -39,7 +39,6 @@ class DataProcessor {
}
static updateSummaries(table, rows) {
- // const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows);
// todo
}
@@ -109,7 +108,7 @@ class DataProcessor {
// todo update sort and filter and ui change
}
- static updatePageDataWithDeleteRecords(deletedRowsIds, table) {
+ static updateDataWithDeleteRecords(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]);
@@ -228,7 +227,7 @@ class DataProcessor {
}
case OPERATION_TYPE.DELETE_RECORDS: {
const { rows_ids } = operation;
- this.updatePageDataWithDeleteRecords(rows_ids, table);
+ this.updateDataWithDeleteRecords(rows_ids, table);
this.updateSummaries();
break;
}
@@ -255,6 +254,10 @@ class DataProcessor {
this.updateSummaries();
break;
}
+ case OPERATION_TYPE.MOVE_RECORD: {
+ this.run(table, { collaborators });
+ break;
+ }
case OPERATION_TYPE.MODIFY_GROUPBYS: {
const { available_columns, groupbys, rows } = table.view;
if (!isGroupView({ groupbys }, available_columns)) {
diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js
index 9dbab8fe4f..7b04c8efdc 100644
--- a/frontend/src/metadata/store/index.js
+++ b/frontend/src/metadata/store/index.js
@@ -12,7 +12,7 @@ import LocalOperator from './local-operator';
import Metadata from '../model/metadata';
import { checkIsDir } from '../utils/row';
import { Utils } from '../../utils/utils';
-import { getFileNameFromRecord } from '../utils/cell';
+import { getFileNameFromRecord, checkDuplicatedName } from '../utils/cell';
class Store {
@@ -300,16 +300,13 @@ class Store {
}
deleteRecords(rows_ids, { fail_callback, success_callback }) {
+ if (!Array.isArray(rows_ids) || rows_ids.length === 0) return;
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 valid_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) => {
@@ -408,6 +405,39 @@ class Store {
this.applyOperation(operation);
}
+ moveRecord(row_id, target_repo_id, dirent, target_parent_path, source_parent_path, update_data, { success_callback, fail_callback }) {
+ const type = OPERATION_TYPE.MOVE_RECORD;
+ const operation = this.createOperation({
+ type,
+ repo_id: this.repoId,
+ row_id,
+ target_repo_id,
+ dirent,
+ target_parent_path,
+ source_parent_path,
+ update_data,
+ success_callback,
+ fail_callback,
+ });
+ this.applyOperation(operation);
+ }
+
+ duplicateRecord(row_id, target_repo_id, dirent, target_parent_path, source_parent_path, { success_callback, fail_callback }) {
+ const type = OPERATION_TYPE.DUPLICATE_RECORD;
+ const operation = this.createOperation({
+ type,
+ repo_id: this.repoId,
+ row_id,
+ target_repo_id,
+ dirent,
+ target_parent_path,
+ source_parent_path,
+ success_callback,
+ fail_callback,
+ });
+ this.applyOperation(operation);
+ }
+
modifyFilters(filterConjunction, filters, basicFilters = []) {
const type = OPERATION_TYPE.MODIFY_FILTERS;
const operation = this.createOperation({
@@ -532,8 +562,7 @@ class Store {
};
checkDuplicatedName = (name, parentDir) => {
- const newPath = Utils.joinPath(parentDir, name);
- return this.data.rows.some((row) => newPath === Utils.joinPath(row._parent_dir, row._name));
+ return checkDuplicatedName(this.data.rows, parentDir, name);
};
renamePeopleName = (peopleId, newName, oldName) => {
diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js
index faf36a2f4a..ee43e4eedf 100644
--- a/frontend/src/metadata/store/operations/apply.js
+++ b/frontend/src/metadata/store/operations/apply.js
@@ -12,41 +12,50 @@ dayjs.extend(utc);
export default function apply(data, operation) {
const { op_type } = operation;
+ const updateDataByModifyRecords = ({ id_original_row_updates = {}, id_row_updates = {} } = {}) => {
+ const { rows } = data;
+ const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
+ const modifier = window.sfMetadataContext.getUsername();
+ let updatedRows = [...rows];
+
+ rows.forEach((row, index) => {
+ const { _id: rowId } = row;
+ const originalRowUpdates = id_original_row_updates[rowId];
+ const rowUpdates = id_row_updates[rowId];
+ 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;
+ };
+
+ const updateDataByDeleteRecords = (deletedIds) => {
+ const idNeedDeletedMap = deletedIds.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {});
+ data.rows = data.rows.filter((row) => !idNeedDeletedMap[row._id]);
+
+ // delete rows in id_row_map
+ deletedIds.forEach(rowId => {
+ delete data.id_row_map[rowId];
+ });
+
+ data.row_ids = data.row_ids.filter(row_id => !idNeedDeletedMap[row_id]);
+ };
+
switch (op_type) {
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 { _id: rowId } = row;
- const originalRowUpdates = id_original_row_updates[rowId];
- const rowUpdates = id_row_updates[rowId];
- 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;
+ updateDataByModifyRecords({ id_original_row_updates, id_row_updates });
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];
- });
-
+ updateDataByDeleteRecords(rows_ids);
return data;
}
case OPERATION_TYPE.RESTORE_RECORDS: {
@@ -117,6 +126,23 @@ export default function apply(data, operation) {
data.rows = updatedRows;
return data;
}
+ case OPERATION_TYPE.MOVE_RECORD: {
+ const { update_data } = operation;
+ const {
+ modify_row_ids: updateRowIds,
+ modify_id_row_updates: idRowUpdates,
+ delete_row_ids: deletedRowIds,
+ } = update_data;
+
+ if (Array.isArray(updateRowIds) && updateRowIds.length > 0) {
+ updateDataByModifyRecords({ id_row_updates: idRowUpdates });
+ }
+
+ if (Array.isArray(deletedRowIds) && deletedRowIds.length > 0) {
+ updateDataByDeleteRecords(deletedRowIds);
+ }
+ return data;
+ }
case OPERATION_TYPE.MODIFY_FILTERS: {
const { filter_conjunction, filters, basic_filters } = operation;
data.view.filter_conjunction = filter_conjunction;
diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js
index 44512b97d4..ca25214fd5 100644
--- a/frontend/src/metadata/store/operations/constants.js
+++ b/frontend/src/metadata/store/operations/constants.js
@@ -1,16 +1,10 @@
export const OPERATION_TYPE = {
- MODIFY_RECORDS: 'modify_records',
- DELETE_RECORDS: 'delete_records',
- RESTORE_RECORDS: 'restore_records',
- RELOAD_RECORDS: 'reload_records',
+ // view
MODIFY_FILTERS: 'modify_filters',
MODIFY_SORTS: 'modify_sorts',
MODIFY_GROUPBYS: 'modify_groupbys',
MODIFY_HIDDEN_COLUMNS: 'modify_hidden_columns',
- LOCK_RECORD_VIA_BUTTON: 'lock_record_via_button',
- MODIFY_RECORD_VIA_BUTTON: 'modify_record_via_button',
MODIFY_SETTINGS: 'modify_settings',
- MODIFY_LOCAL_RECORD: 'modify_local_record',
// column
INSERT_COLUMN: 'insert_column',
@@ -20,6 +14,17 @@ export const OPERATION_TYPE = {
MODIFY_COLUMN_WIDTH: 'modify_column_width',
MODIFY_COLUMN_ORDER: 'modify_column_order',
+ // record
+ MODIFY_RECORDS: 'modify_records',
+ DELETE_RECORDS: 'delete_records',
+ RESTORE_RECORDS: 'restore_records',
+ RELOAD_RECORDS: 'reload_records',
+ LOCK_RECORD_VIA_BUTTON: 'lock_record_via_button',
+ MODIFY_RECORD_VIA_BUTTON: 'modify_record_via_button',
+ MODIFY_LOCAL_RECORD: 'modify_local_record',
+ MOVE_RECORD: 'move_record',
+ DUPLICATE_RECORD: 'duplicate_record',
+
// face table
RENAME_PEOPLE_NAME: 'rename_people_name',
DELETE_PEOPLE_PHOTOS: 'delete_people_photos',
@@ -41,6 +46,8 @@ export const OPERATION_ATTRIBUTES = {
[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', 'deleted_rows'],
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
+ [OPERATION_TYPE.MOVE_RECORD]: ['repo_id', 'row_id', 'target_repo_id', 'dirent', 'target_parent_path', 'source_parent_path', 'update_data'],
+ [OPERATION_TYPE.DUPLICATE_RECORD]: ['repo_id', 'row_id', 'target_repo_id', 'dirent', 'target_parent_path', 'source_parent_path'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters', 'basic_filters'],
[OPERATION_TYPE.MODIFY_SORTS]: ['repo_id', 'view_id', 'sorts'],
@@ -83,6 +90,8 @@ export const NEED_APPLY_AFTER_SERVER_OPERATION = [
OPERATION_TYPE.INSERT_COLUMN,
OPERATION_TYPE.MODIFY_FILTERS,
OPERATION_TYPE.MODIFY_SORTS,
+ OPERATION_TYPE.MOVE_RECORD,
+ OPERATION_TYPE.DUPLICATE_RECORD,
];
export const VIEW_OPERATION = [
diff --git a/frontend/src/metadata/store/server-operator.js b/frontend/src/metadata/store/server-operator.js
index e93ca8b4e5..5b4d04d182 100644
--- a/frontend/src/metadata/store/server-operator.js
+++ b/frontend/src/metadata/store/server-operator.js
@@ -82,6 +82,30 @@ class ServerOperator {
callback({ operation });
break;
}
+ case OPERATION_TYPE.MOVE_RECORD: {
+ const { row_id, repo_id, target_repo_id, dirent, target_parent_path, source_parent_path } = operation;
+ seafileAPI.moveDir(repo_id, target_repo_id, target_parent_path, source_parent_path, dirent.name).then(res => {
+ operation.task_id = res.data.task_id || null;
+ callback({ operation });
+ }).catch(error => {
+ const row = getRowById(data, row_id);
+ const isDir = checkIsDir(row);
+ callback({ error: isDir ? gettext('Failed to move folder') : gettext('Failed to move file') });
+ });
+ break;
+ }
+ case OPERATION_TYPE.DUPLICATE_RECORD: {
+ const { row_id, repo_id, target_repo_id, dirent, target_parent_path, source_parent_path } = operation;
+ seafileAPI.copyDir(repo_id, target_repo_id, target_parent_path, source_parent_path, dirent.name).then(res => {
+ operation.task_id = res.data.task_id || null;
+ callback({ operation });
+ }).catch(error => {
+ const row = getRowById(data, row_id);
+ const isDir = checkIsDir(row);
+ callback({ error: isDir ? gettext('Failed to duplicate folder') : gettext('Failed to duplicate file') });
+ });
+ break;
+ }
case OPERATION_TYPE.INSERT_COLUMN: {
const { repo_id, name, column_type, column_key, data } = operation;
window.sfMetadataContext.insertColumn(repo_id, name, column_type, { key: column_key, data }).then(res => {
diff --git a/frontend/src/metadata/utils/cell/column/file-name.js b/frontend/src/metadata/utils/cell/column/file-name.js
new file mode 100644
index 0000000000..0ca859671a
--- /dev/null
+++ b/frontend/src/metadata/utils/cell/column/file-name.js
@@ -0,0 +1,17 @@
+import { Utils } from '../../../../utils/utils';
+
+export const checkDuplicatedName = (rows, parentDir, fileName) => {
+ if (!Array.isArray(rows) || rows.length === 0 || !parentDir || !fileName) return false;
+ const newPath = Utils.joinPath(parentDir, fileName);
+ return rows.some((row) => newPath === Utils.joinPath(row._parent_dir, row._name));
+};
+
+export const getUniqueFileName = (rows, parentDir, fileName) => {
+ const dotIndex = fileName.lastIndexOf('.');
+ const fileType = dotIndex === -1 ? '' : fileName.slice(dotIndex + 1);
+ let newName = dotIndex === -1 ? fileName : fileName.slice(0, dotIndex);
+ while (checkDuplicatedName(rows, parentDir, dotIndex === -1 ? newName : `${newName}.${fileType}`)) {
+ newName = newName + ' (1)';
+ }
+ return dotIndex === -1 ? newName : `${newName}.${fileType}`;
+};
diff --git a/frontend/src/metadata/utils/cell/column/index.js b/frontend/src/metadata/utils/cell/column/index.js
index 6e6a10f71d..30856f24ba 100644
--- a/frontend/src/metadata/utils/cell/column/index.js
+++ b/frontend/src/metadata/utils/cell/column/index.js
@@ -31,3 +31,7 @@ export {
export {
getTagsDisplayString,
} from './tag';
+export {
+ checkDuplicatedName,
+ getUniqueFileName,
+} from './file-name';
diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js
index bde8d065ec..ed79d0b26a 100644
--- a/frontend/src/metadata/views/gallery/context-menu/index.js
+++ b/frontend/src/metadata/views/gallery/context-menu/index.js
@@ -9,30 +9,47 @@ import metadataAPI from '../../../api';
import toaster from '../../../../components/toast';
import { Utils } from '../../../../utils/utils';
import ModalPortal from '../../../../components/modal-portal';
+import CopyDirent from '../../../../components/dialog/copy-dirent-dialog';
+import { Dirent } from '../../../../models';
const CONTEXT_MENU_KEY = {
DOWNLOAD: 'download',
DELETE: 'delete',
+ DUPLICATE: 'duplicate',
};
-const GalleryContextMenu = ({ metadata, selectedImages, boundaryCoordinates, onDelete }) => {
+const GalleryContextMenu = ({ metadata, selectedImages, boundaryCoordinates, onDelete, onDuplicate, addFolder }) => {
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
+ const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
const repoID = window.sfMetadataContext.getSetting('repoID');
const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
+ const canDuplicateRow = window.sfMetadataContext.canDuplicateRow();
const options = useMemo(() => {
let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }];
if (checkCanDeleteRow) {
validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: selectedImages.length > 1 ? gettext('Delete') : gettext('Delete file') });
}
+ if (canDuplicateRow && selectedImages.length === 1) {
+ validOptions.push({ value: CONTEXT_MENU_KEY.DUPLICATE, label: gettext('Duplicate') });
+ }
return validOptions;
- }, [checkCanDeleteRow, selectedImages]);
+ }, [checkCanDeleteRow, canDuplicateRow, selectedImages]);
const closeZipDialog = () => {
setIsZipDialogOpen(false);
};
+ const toggleCopyDialog = useCallback(() => {
+ setIsCopyDialogOpen(!isCopyDialogOpen);
+ }, [isCopyDialogOpen]);
+
+ const handleDuplicate = useCallback((destRepo, dirent, destPath, nodeParentPath, isByDialog) => {
+ const selectedImage = selectedImages[0];
+ onDuplicate(selectedImage.id, destRepo, dirent, destPath, nodeParentPath, isByDialog);
+ }, [selectedImages, onDuplicate]);
+
const handleDownload = useCallback(() => {
if (!selectedImages.length) return;
if (selectedImages.length === 1) {
@@ -66,10 +83,16 @@ const GalleryContextMenu = ({ metadata, selectedImages, boundaryCoordinates, onD
case 'delete':
onDelete(selectedImages);
break;
+ case CONTEXT_MENU_KEY.DUPLICATE:
+ toggleCopyDialog();
+ break;
default:
break;
}
- }, [selectedImages, handleDownload, onDelete]);
+ }, [selectedImages, handleDownload, onDelete, toggleCopyDialog]);
+
+ const dirent = new Dirent({ name: selectedImages[0]?.name });
+ const path = selectedImages[0]?.path;
return (
<>
@@ -89,6 +112,20 @@ const GalleryContextMenu = ({ metadata, selectedImages, boundaryCoordinates, onD
/>
)}
+ {isCopyDialogOpen && (
+