1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-26 07:22:34 +00:00

Feature/table operations (#8149)

* add move/copy/download operations on table rows

* optimize

* fix permission

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-08-30 11:13:23 +08:00
committed by GitHub
parent b94d3218c3
commit 4d1d71c85d
10 changed files with 453 additions and 45 deletions

View File

@@ -8,7 +8,7 @@ import { getFileName } from '../../tag/utils/file';
import RowUtils from '../../metadata/views/table/utils/row-utils';
import { checkIsDir } from '../../metadata/utils/row';
import { Utils } from '../../utils/utils';
import { getFileNameFromRecord } from '../../metadata/utils/cell';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../metadata/utils/cell';
import { getColumnByKey } from '../../metadata/utils/column';
import { openInNewTab, openParentFolder } from '../../metadata/utils/file';
@@ -22,6 +22,12 @@ const TableFilesToolbar = ({ repoID }) => {
const records = useMemo(() => selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean) || [], [selectedRecordIds]);
const areRecordsInSameFolder = useMemo(() => {
if (records.length <= 1) return true;
const firstPath = records[0] ? getParentDirFromRecord(records[0]) : null;
return firstPath && records.every(record => getParentDirFromRecord(record) === firstPath);
}, [records]);
const unSelect = useCallback(() => {
setSelectedRecordIds([]);
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []);
@@ -37,16 +43,32 @@ const TableFilesToolbar = ({ repoID }) => {
}, [eventBus, selectedRecordIds]);
const toggleMoveDialog = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records[0]);
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records);
}, [eventBus, records]);
const toggleCopyDialog = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, records);
}, [eventBus, records]);
const downloadRecords = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, selectedRecordIds);
}, [eventBus, selectedRecordIds]);
const checkCanModifyRow = (row) => window.sfMetadataContext.canModifyRow(row);
const getMenuList = useCallback(() => {
const { EXTRACT_FILE_DETAIL, EXTRACT_FILE_DETAILS, OPEN_FILE_IN_NEW_TAB, OPEN_FOLDER_IN_NEW_TAB, OPEN_PARENT_FOLDER, GENERATE_DESCRIPTION, OCR } = TextTranslation;
const { EXTRACT_FILE_DETAIL, EXTRACT_FILE_DETAILS, OPEN_FILE_IN_NEW_TAB, OPEN_FOLDER_IN_NEW_TAB, OPEN_PARENT_FOLDER, GENERATE_DESCRIPTION, OCR, COPY, DOWNLOAD, MOVE } = TextTranslation;
const length = selectedRecordIds.length;
const list = [];
if (length > 1) {
if (areRecordsInSameFolder) {
if (canModify) {
list.push(MOVE);
list.push(COPY);
}
list.push(DOWNLOAD);
}
if (enableSeafileAI) {
const imageOrVideoRecords = records.filter(record => {
if (checkIsDir(record) || !checkCanModifyRow(record)) return false;
@@ -54,6 +76,7 @@ const TableFilesToolbar = ({ repoID }) => {
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
});
if (imageOrVideoRecords.length > 0) {
list.push('Divider');
list.push(EXTRACT_FILE_DETAILS);
}
}
@@ -69,6 +92,8 @@ const TableFilesToolbar = ({ repoID }) => {
list.push(OPEN_PARENT_FOLDER);
const modifyOptions = [];
modifyOptions.push(COPY);
modifyOptions.push(DOWNLOAD);
if (modifyOptions.length > 0) {
list.push('Divider');
@@ -103,11 +128,23 @@ const TableFilesToolbar = ({ repoID }) => {
}
}
return list;
}, [selectedRecordIds, records]);
}, [selectedRecordIds, records, canModify, areRecordsInSameFolder]);
const onMenuItemClick = useCallback((operation) => {
const records = selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean);
switch (operation) {
case TextTranslation.MOVE.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records);
break;
}
case TextTranslation.COPY.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, records);
break;
}
case TextTranslation.DOWNLOAD.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, selectedRecordIds);
break;
}
case TextTranslation.EXTRACT_FILE_DETAIL.key:
case TextTranslation.EXTRACT_FILE_DETAILS.key: {
const imageOrVideoRecords = records.filter(record => {
@@ -172,6 +209,23 @@ const TableFilesToolbar = ({ repoID }) => {
</span>
</>
}
{(length > 1 && canModify && areRecordsInSameFolder) &&
<>
<span className="cur-view-path-btn" onClick={toggleMoveDialog}>
<span className="sf3-font-move1 sf3-font" aria-label={gettext('Move')} title={gettext('Move')}></span>
</span>
</>
}
{((length === 1) || (length > 1 && areRecordsInSameFolder)) &&
<>
<span className="cur-view-path-btn" onClick={toggleCopyDialog}>
<span className="sf3-font-copy1 sf3-font" aria-label={gettext('Copy')} title={gettext('Copy')}></span>
</span>
<span className="cur-view-path-btn" onClick={downloadRecords}>
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')} title={gettext('Download')}></span>
</span>
</>
}
{canModify &&
<span className="cur-view-path-btn" onClick={deleteRecords}>
<span className="sf3-font-delete1 sf3-font" aria-label={gettext('Delete')} title={gettext('Delete')}></span>

View File

@@ -43,6 +43,9 @@ export const EVENT_BUS_TYPE = {
TOGGLE_MOVE_DIALOG: 'toggle_move_dialog',
MOVE_RECORD: 'move_record',
DUPLICATE_RECORD: 'duplicate_record',
TOGGLE_COPY_DIALOG: 'toggle_copy_dialog',
COPY_RECORDS: 'copy_records',
DOWNLOAD_RECORDS: 'download_records',
DELETE_RECORDS: 'delete_records',
UPDATE_RECORD_DETAILS: 'update_record_details',
UPDATE_FACE_RECOGNITION: 'update_face_recognition',

View File

@@ -38,7 +38,7 @@ export const MetadataViewProvider = ({
const { collaborators, collaboratorsCache } = useCollaborators();
const { isBeingBuilt, setIsBeingBuilt } = useMetadata();
const { onOCR: OCRAPI, generateDescription, extractFilesDetails, faceRecognition, generateFileTags: generateFileTagsAPI } = useMetadataAIOperations();
const { handleMove } = useFileOperations();
const { handleMove, handleCopy, handleDownload } = useFileOperations();
const { globalHiddenColumns } = useMetadataStatus();
const storeRef = useRef(null);
@@ -419,16 +419,206 @@ export const MetadataViewProvider = ({
});
}, [updateFileTags, generateFileTagsAPI]);
const handleMoveRecord = (record) => {
const path = getParentDirFromRecord(record);
const currentRecordId = getRecordIdFromRecord(record);
const fileName = getFileNameFromRecord(record);
const dirent = new Dirent({ name: fileName });
const callback = (...params) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
moveRecord && moveRecord(currentRecordId, ...params);
const areRecordsInSameFolder = useCallback((records) => {
if (!records || records.length <= 1) return true;
const firstPath = getParentDirFromRecord(records[0]);
return records.every(record => getParentDirFromRecord(record) === firstPath);
}, []);
const calculateBatchMoveUpdateData = useCallback((records, targetRepoId, targetParentPath, sourceParentPath) => {
const { rows } = storeRef.current.data || metadata;
let needDeletedRowIds = [];
let updateRowIds = [];
let idRowUpdates = {};
let idOldRowData = {};
records.forEach(record => {
const rowId = getRecordIdFromRecord(record);
const isDir = checkIsDir(record);
const oldName = getFileNameFromRecord(record);
const oldParentPath = Utils.joinPath(sourceParentPath, oldName);
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.push(rowId);
if (isDir) {
rows.forEach((row) => {
const parentDir = getParentDirFromRecord(row);
if (row && parentDir.startsWith(oldParentPath)) {
const id = getRecordIdFromRecord(row);
needDeletedRowIds.push(id);
}
});
}
}
});
return {
modify_row_ids: updateRowIds,
modify_id_row_updates: idRowUpdates,
modify_id_old_row_data: idOldRowData,
delete_row_ids: needDeletedRowIds,
};
handleMove(path, dirent, false, callback);
}, [metadata, repoID]);
const handleMoveRecords = (records) => {
if (records.length > 1) {
if (!areRecordsInSameFolder(records)) {
toaster.danger(gettext('Can only move files that are in the same folder'));
return;
}
const path = getParentDirFromRecord(records[0]);
const recordIds = records.map(r => getRecordIdFromRecord(r));
const dirents = records.map(r => getFileNameFromRecord(r));
const selectedDirentList = records.map(r => {
const fileName = getFileNameFromRecord(r);
return new Dirent({ name: fileName, is_dir: checkIsDir(r) });
});
const callback = (destRepo, destDirentPath, isByDialog = false) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
const updateData = calculateBatchMoveUpdateData(records, destRepo.repo_id, destDirentPath, path);
storeRef.current.moveRecords(recordIds, destRepo.repo_id, dirents, destDirentPath, path, updateData, {
success_callback: (operation) => {
if (selectedDirentList.length > 0) {
moveFileCallback && moveFileCallback(
repoID,
destRepo,
selectedDirentList[0],
destDirentPath,
path,
operation.task_id,
isByDialog,
{
isBatchOperation: true,
batchFileNames: dirents,
}
);
}
},
fail_callback: (error) => {
error && toaster.danger(error);
}
});
};
handleMove(path, selectedDirentList, true, callback);
} else {
// Single record
const path = getParentDirFromRecord(records[0]);
const currentRecordId = getRecordIdFromRecord(records[0]);
const fileName = getFileNameFromRecord(records[0]);
const dirent = new Dirent({ name: fileName, is_dir: checkIsDir(records[0]) });
const callback = (...params) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
moveRecord && moveRecord(currentRecordId, ...params);
};
handleMove(path, dirent, false, callback);
}
};
const handleCopyRecords = (records) => {
if (records.length > 1) {
if (!areRecordsInSameFolder(records)) {
toaster.danger(gettext('Can only copy files that are in the same folder'));
return;
}
const path = getParentDirFromRecord(records[0]);
const recordIds = records.map(r => getRecordIdFromRecord(r));
const dirents = records.map(r => getFileNameFromRecord(r));
const selectedDirentList = records.map(r => {
const fileName = getFileNameFromRecord(r);
return new Dirent({ name: fileName, is_dir: checkIsDir(r) });
});
const callback = (destRepo, destDirentPath, isByDialog = false) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
storeRef.current.duplicateRecords(recordIds, destRepo.repo_id, dirents, destDirentPath, path, {
success_callback: (operation) => {
if (selectedDirentList.length > 0) {
copyFileCallback && copyFileCallback(
repoID,
destRepo,
selectedDirentList[0],
destDirentPath,
path,
operation.task_id,
isByDialog,
{
isBatchOperation: true,
batchFileNames: dirents,
}
);
}
if (repoID === destRepo.repo_id) {
delayReloadMetadata();
}
},
fail_callback: (error) => {
error && toaster.danger(error);
}
});
};
handleCopy(path, selectedDirentList, true, callback);
} else {
// Single record
const path = getParentDirFromRecord(records[0]);
const currentRecordId = getRecordIdFromRecord(records[0]);
const fileName = getFileNameFromRecord(records[0]);
const dirent = new Dirent({ name: fileName, is_dir: checkIsDir(records[0]) });
const callback = (...params) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
duplicateRecord && duplicateRecord(currentRecordId, ...params);
};
handleCopy(path, dirent, false, callback);
}
};
const handleDownloadRecords = (recordIds) => {
if (recordIds.length === 0) return;
const records = recordIds.map(id => storeRef.current?.data?.id_row_map?.[id]).filter(Boolean);
if (records.length === 0) return;
if (!areRecordsInSameFolder(records)) {
toaster.danger(gettext('Can only download files that are in the same folder'));
return;
}
const path = getParentDirFromRecord(records[0]);
const direntList = records.map(record => {
const fileName = getFileNameFromRecord(record);
return { name: fileName, is_dir: checkIsDir(record) };
});
handleDownload(path, direntList);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
};
const getSearchableValue = useCallback((row, column, collaborators, tagsData, collaboratorsCache) => {
@@ -639,7 +829,9 @@ export const MetadataViewProvider = ({
const unsubscribeUpdateFaceRecognition = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, updateFaceRecognition);
const unsubscribeUpdateDescription = eventBus.subscribe(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, updateRecordDescription);
const unsubscribeOCR = eventBus.subscribe(EVENT_BUS_TYPE.OCR, onOCR);
const unsubscribeToggleMoveDialog = eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, handleMoveRecord);
const unsubscribeToggleMoveDialog = eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, handleMoveRecords);
const unsubscribeToggleCopyDialog = eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, handleCopyRecords);
const unsubscribeDownloadRecords = eventBus.subscribe(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, handleDownloadRecords);
const unsubscribeSearchRows = eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_ROWS, handleSearchRows);
return () => {
@@ -669,6 +861,8 @@ export const MetadataViewProvider = ({
unsubscribeUpdateDescription();
unsubscribeOCR();
unsubscribeToggleMoveDialog();
unsubscribeToggleCopyDialog();
unsubscribeDownloadRecords();
unsubscribeSearchRows();
delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current);
};

View File

@@ -255,7 +255,9 @@ class DataProcessor {
this.updateSummaries();
break;
}
case OPERATION_TYPE.MOVE_RECORD: {
case OPERATION_TYPE.MOVE_RECORD:
case OPERATION_TYPE.DUPLICATE_RECORD:
case OPERATION_TYPE.DUPLICATE_RECORDS: {
this.run(table, { collaborators });
break;
}

View File

@@ -436,6 +436,23 @@ class Store {
this.applyOperation(operation);
}
moveRecords(row_ids, target_repo_id, dirents, target_parent_path, source_parent_path, update_data, { success_callback, fail_callback }) {
const type = OPERATION_TYPE.MOVE_RECORDS;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_ids,
target_repo_id,
dirents,
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({
@@ -452,6 +469,22 @@ class Store {
this.applyOperation(operation);
}
duplicateRecords(row_ids, target_repo_id, dirents, target_parent_path, source_parent_path, { success_callback, fail_callback }) {
const type = OPERATION_TYPE.DUPLICATE_RECORDS;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_ids,
target_repo_id,
dirents,
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({

View File

@@ -128,7 +128,8 @@ export default function apply(data, operation) {
data.rows = updatedRows;
return data;
}
case OPERATION_TYPE.MOVE_RECORD: {
case OPERATION_TYPE.MOVE_RECORD:
case OPERATION_TYPE.MOVE_RECORDS: {
const { update_data } = operation;
const {
modify_row_ids: updateRowIds,
@@ -145,6 +146,11 @@ export default function apply(data, operation) {
}
return data;
}
case OPERATION_TYPE.DUPLICATE_RECORD:
case OPERATION_TYPE.DUPLICATE_RECORDS: {
// Duplicate operations don't modify local data immediately
return data;
}
case OPERATION_TYPE.MODIFY_FILTERS: {
const { filter_conjunction, filters, basic_filters } = operation;
data.view.filter_conjunction = filter_conjunction;

View File

@@ -26,7 +26,9 @@ export const OPERATION_TYPE = {
MODIFY_RECORD_VIA_BUTTON: 'modify_record_via_button',
MODIFY_LOCAL_RECORD: 'modify_local_record',
MOVE_RECORD: 'move_record',
MOVE_RECORDS: 'move_records',
DUPLICATE_RECORD: 'duplicate_record',
DUPLICATE_RECORDS: 'duplicate_records',
// face table
RENAME_PEOPLE_NAME: 'rename_people_name',
@@ -54,6 +56,8 @@ export const OPERATION_ATTRIBUTES = {
[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.MOVE_RECORDS]: ['repo_id', 'row_ids', 'target_repo_id', 'dirents', 'target_parent_path', 'source_parent_path', 'update_data'],
[OPERATION_TYPE.DUPLICATE_RECORDS]: ['repo_id', 'row_ids', 'target_repo_id', 'dirents', 'target_parent_path', 'source_parent_path'],
[OPERATION_TYPE.MODIFY_LOCAL_RECORD]: ['repo_id', 'row_id', 'parent_dir', 'file_name', 'updates'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters', 'basic_filters'],
@@ -109,6 +113,8 @@ export const NEED_APPLY_AFTER_SERVER_OPERATION = [
OPERATION_TYPE.MODIFY_SORTS,
OPERATION_TYPE.MOVE_RECORD,
OPERATION_TYPE.DUPLICATE_RECORD,
OPERATION_TYPE.MOVE_RECORDS,
OPERATION_TYPE.DUPLICATE_RECORDS,
];
export const VIEW_OPERATION = [

View File

@@ -98,6 +98,20 @@ class ServerOperator {
});
break;
}
case OPERATION_TYPE.MOVE_RECORDS: {
const { repo_id, target_repo_id, dirents, target_parent_path, source_parent_path } = operation;
seafileAPI.moveDir(repo_id, target_repo_id, target_parent_path, source_parent_path, dirents).then(res => {
operation.task_id = res.data.task_id || null;
callback({ operation });
}).catch(error => {
const count = dirents.length;
const errorMsg = count > 1
? gettext('Failed to move {n} items').replace('{n}', count)
: gettext('Failed to move file');
callback({ error: errorMsg });
});
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 => {
@@ -110,6 +124,20 @@ class ServerOperator {
});
break;
}
case OPERATION_TYPE.DUPLICATE_RECORDS: {
const { repo_id, target_repo_id, dirents, target_parent_path, source_parent_path } = operation;
seafileAPI.copyDir(repo_id, target_repo_id, target_parent_path, source_parent_path, dirents).then(res => {
operation.task_id = res.data.task_id || null;
callback({ operation });
}).catch(error => {
const count = dirents.length;
const errorMsg = count > 1
? gettext('Failed to copy {n} items').replace('{n}', count)
: gettext('Failed to copy file');
callback({ error: errorMsg });
});
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 => {

View File

@@ -29,6 +29,8 @@ const OPERATION = {
FILE_DETAILS: 'file-details',
DETECT_FACES: 'detect-faces',
MOVE: 'move',
COPY: 'copy',
DOWNLOAD: 'download',
};
const ContextMenu = ({
@@ -119,7 +121,6 @@ const ContextMenu = ({
return list;
}
// handle selected records
const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : [];
if (selectedRecordsIds.length > 1) {
let records = [];
@@ -130,6 +131,20 @@ const ContextMenu = ({
}
});
const areRecordsInSameFolder = (() => {
if (records.length <= 1) return true;
const firstPath = getParentDirFromRecord(records[0]);
return records.every(record => getParentDirFromRecord(record) === firstPath);
})();
if (areRecordsInSameFolder) {
if (!isReadonly) {
list.push({ value: OPERATION.MOVE, label: gettext('Move'), records });
list.push({ value: OPERATION.COPY, label: gettext('Copy'), records });
}
list.push({ value: OPERATION.DOWNLOAD, label: gettext('Download'), records });
}
const ableDeleteRecords = getAbleDeleteRecords(records);
if (ableDeleteRecords.length > 0) {
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords });
@@ -177,6 +192,10 @@ const ContextMenu = ({
if (canModifyRow) {
modifyOptions.push({ value: OPERATION.MOVE, label: isFolder ? gettext('Move folder') : gettext('Move file'), record });
}
// Add copy and download options for single record
modifyOptions.push({ value: OPERATION.COPY, label: isFolder ? gettext('Copy folder') : gettext('Copy file'), record });
modifyOptions.push({ value: OPERATION.DOWNLOAD, label: isFolder ? gettext('Download folder') : gettext('Download file'), record });
if (canDeleteRow) {
modifyOptions.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record });
}
@@ -301,9 +320,33 @@ const ContextMenu = ({
break;
}
case OPERATION.MOVE: {
const { record } = option;
if (!record) break;
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, record);
const { record, records } = option;
if (records) {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, records);
} else if (record) {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, [record]);
}
break;
}
case OPERATION.COPY: {
const { record, records } = option;
if (records) {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, records);
} else if (record) {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_COPY_DIALOG, [record]);
}
break;
}
case OPERATION.DOWNLOAD: {
const { record, records } = option;
if (records) {
// Multiple records
const recordIds = records.map(r => r._id).filter(Boolean);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, recordIds);
} else if (record) {
// Single record
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_RECORDS, [record._id]);
}
break;
}
default: {

View File

@@ -1305,15 +1305,23 @@ class LibContentView extends React.Component {
};
// list operations
moveItemsAjaxCallback = (repoID, targetRepo, dirent, moveToDirentPath, nodeParentPath, taskId, byDialog = false) => {
moveItemsAjaxCallback = (repoID, targetRepo, dirent, moveToDirentPath, nodeParentPath, taskId, byDialog = false, options = {}) => {
this.updateCurrentNotExistDirent(dirent);
const {
isBatchOperation = false,
batchFileNames = [],
customMessage = null
} = options;
const dirName = dirent.name;
const direntPath = Utils.joinPath(nodeParentPath, dirName);
if (repoID !== targetRepo.repo_id) {
const operatedFilesLength = isBatchOperation ? batchFileNames.length : 1;
this.setState({
asyncCopyMoveTaskId: taskId,
asyncOperatedFilesLength: 1,
asyncOperatedFilesLength: operatedFilesLength,
asyncOperationProgress: 0,
asyncOperationType: 'move',
isCopyMoveProgressDialogShow: true,
@@ -1324,28 +1332,38 @@ class LibContentView extends React.Component {
});
}
// 1. move to current repo
// 2. tow columns mode need update left tree
const updateAfterMove = () => {
if (repoID === targetRepo.repo_id && this.state.isTreePanelShown) {
this.updateMoveCopyTreeNode(moveToDirentPath);
}
this.moveDirent(direntPath, moveToDirentPath);
};
if (this.state.isTreePanelShown) {
this.deleteTreeNode(direntPath, updateAfterMove);
} else {
updateAfterMove();
if (isBatchOperation) {
const direntPaths = batchFileNames.map(fileName => Utils.joinPath(nodeParentPath, fileName));
this.deleteTreeNodes(direntPaths, () => {
if (repoID === targetRepo.repo_id) {
this.updateMoveCopyTreeNode(moveToDirentPath);
}
});
} else {
this.deleteTreeNode(direntPath, () => {
if (repoID === targetRepo.repo_id) {
this.updateMoveCopyTreeNode(moveToDirentPath);
}
});
}
}
// show tip message if move to current repo
if (repoID === targetRepo.repo_id) {
let message = gettext('Successfully moved {name}.');
let message;
if (customMessage) {
message = customMessage;
} else if (isBatchOperation && batchFileNames.length > 1) {
message = gettext('Successfully moved {name} and {n} other items');
message = message.replace('{name}', batchFileNames[0])
.replace('{n}', batchFileNames.length - 1);
} else {
message = gettext('Successfully moved {name}.');
message = message.replace('{name}', dirName);
toaster.success(message);
}
toaster.success(message);
if (byDialog) {
this.updateRecentlyUsedList(targetRepo, moveToDirentPath);
}
@@ -1382,12 +1400,20 @@ class LibContentView extends React.Component {
});
};
copyItemsAjaxCallback = (repoID, targetRepo, dirent, copyToDirentPath, nodeParentPath, taskId, byDialog = false) => {
copyItemsAjaxCallback = (repoID, targetRepo, dirent, copyToDirentPath, nodeParentPath, taskId, byDialog = false, options = {}) => {
this.onSelectedDirentListUpdate([]);
const {
isBatchOperation = false,
batchFileNames = [],
customMessage = null
} = options;
if (repoID !== targetRepo.repo_id) {
const operatedFilesLength = isBatchOperation ? batchFileNames.length : 1;
this.setState({
asyncCopyMoveTaskId: taskId,
asyncOperatedFilesLength: 1,
asyncOperatedFilesLength: operatedFilesLength,
asyncOperationProgress: 0,
asyncOperationType: 'copy',
isCopyMoveProgressDialogShow: true
@@ -1409,8 +1435,19 @@ class LibContentView extends React.Component {
}
const dirName = dirent.name;
let message = gettext('Successfully copied %(name)s.');
message = message.replace('%(name)s', dirName);
let message;
if (customMessage) {
message = customMessage;
} else if (isBatchOperation && batchFileNames.length > 1) {
message = gettext('Successfully copied {name} and {n} other items');
message = message.replace('{name}', batchFileNames[0])
.replace('{n}', batchFileNames.length - 1);
} else {
message = gettext('Successfully copied %(name)s.');
message = message.replace('%(name)s', dirName);
}
toaster.success(message);
if (byDialog) {
@@ -2068,9 +2105,11 @@ class LibContentView extends React.Component {
});
};
deleteTreeNodes = (paths) => {
deleteTreeNodes = (paths, callback) => {
let tree = treeHelper.deleteNodeListByPaths(this.state.treeData, paths);
this.setState({ treeData: tree });
this.setState({ treeData: tree }, () => {
callback && callback();
});
};
moveTreeNode = (nodePath, moveToPath, moveToRepo, nodeName) => {