1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-26 15:26:19 +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 RowUtils from '../../metadata/views/table/utils/row-utils';
import { checkIsDir } from '../../metadata/utils/row'; import { checkIsDir } from '../../metadata/utils/row';
import { Utils } from '../../utils/utils'; 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 { getColumnByKey } from '../../metadata/utils/column';
import { openInNewTab, openParentFolder } from '../../metadata/utils/file'; 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 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(() => { const unSelect = useCallback(() => {
setSelectedRecordIds([]); setSelectedRecordIds([]);
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []); eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []);
@@ -37,16 +43,32 @@ const TableFilesToolbar = ({ repoID }) => {
}, [eventBus, selectedRecordIds]); }, [eventBus, selectedRecordIds]);
const toggleMoveDialog = useCallback(() => { 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]); }, [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 checkCanModifyRow = (row) => window.sfMetadataContext.canModifyRow(row);
const getMenuList = useCallback(() => { 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 length = selectedRecordIds.length;
const list = []; const list = [];
if (length > 1) { if (length > 1) {
if (areRecordsInSameFolder) {
if (canModify) {
list.push(MOVE);
list.push(COPY);
}
list.push(DOWNLOAD);
}
if (enableSeafileAI) { if (enableSeafileAI) {
const imageOrVideoRecords = records.filter(record => { const imageOrVideoRecords = records.filter(record => {
if (checkIsDir(record) || !checkCanModifyRow(record)) return false; if (checkIsDir(record) || !checkCanModifyRow(record)) return false;
@@ -54,6 +76,7 @@ const TableFilesToolbar = ({ repoID }) => {
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName); return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
}); });
if (imageOrVideoRecords.length > 0) { if (imageOrVideoRecords.length > 0) {
list.push('Divider');
list.push(EXTRACT_FILE_DETAILS); list.push(EXTRACT_FILE_DETAILS);
} }
} }
@@ -69,6 +92,8 @@ const TableFilesToolbar = ({ repoID }) => {
list.push(OPEN_PARENT_FOLDER); list.push(OPEN_PARENT_FOLDER);
const modifyOptions = []; const modifyOptions = [];
modifyOptions.push(COPY);
modifyOptions.push(DOWNLOAD);
if (modifyOptions.length > 0) { if (modifyOptions.length > 0) {
list.push('Divider'); list.push('Divider');
@@ -103,11 +128,23 @@ const TableFilesToolbar = ({ repoID }) => {
} }
} }
return list; return list;
}, [selectedRecordIds, records]); }, [selectedRecordIds, records, canModify, areRecordsInSameFolder]);
const onMenuItemClick = useCallback((operation) => { const onMenuItemClick = useCallback((operation) => {
const records = selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean); const records = selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean);
switch (operation) { 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_DETAIL.key:
case TextTranslation.EXTRACT_FILE_DETAILS.key: { case TextTranslation.EXTRACT_FILE_DETAILS.key: {
const imageOrVideoRecords = records.filter(record => { const imageOrVideoRecords = records.filter(record => {
@@ -172,6 +209,23 @@ const TableFilesToolbar = ({ repoID }) => {
</span> </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 && {canModify &&
<span className="cur-view-path-btn" onClick={deleteRecords}> <span className="cur-view-path-btn" onClick={deleteRecords}>
<span className="sf3-font-delete1 sf3-font" aria-label={gettext('Delete')} title={gettext('Delete')}></span> <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', TOGGLE_MOVE_DIALOG: 'toggle_move_dialog',
MOVE_RECORD: 'move_record', MOVE_RECORD: 'move_record',
DUPLICATE_RECORD: 'duplicate_record', DUPLICATE_RECORD: 'duplicate_record',
TOGGLE_COPY_DIALOG: 'toggle_copy_dialog',
COPY_RECORDS: 'copy_records',
DOWNLOAD_RECORDS: 'download_records',
DELETE_RECORDS: 'delete_records', DELETE_RECORDS: 'delete_records',
UPDATE_RECORD_DETAILS: 'update_record_details', UPDATE_RECORD_DETAILS: 'update_record_details',
UPDATE_FACE_RECOGNITION: 'update_face_recognition', UPDATE_FACE_RECOGNITION: 'update_face_recognition',

View File

@@ -38,7 +38,7 @@ export const MetadataViewProvider = ({
const { collaborators, collaboratorsCache } = useCollaborators(); const { collaborators, collaboratorsCache } = useCollaborators();
const { isBeingBuilt, setIsBeingBuilt } = useMetadata(); const { isBeingBuilt, setIsBeingBuilt } = useMetadata();
const { onOCR: OCRAPI, generateDescription, extractFilesDetails, faceRecognition, generateFileTags: generateFileTagsAPI } = useMetadataAIOperations(); const { onOCR: OCRAPI, generateDescription, extractFilesDetails, faceRecognition, generateFileTags: generateFileTagsAPI } = useMetadataAIOperations();
const { handleMove } = useFileOperations(); const { handleMove, handleCopy, handleDownload } = useFileOperations();
const { globalHiddenColumns } = useMetadataStatus(); const { globalHiddenColumns } = useMetadataStatus();
const storeRef = useRef(null); const storeRef = useRef(null);
@@ -419,16 +419,206 @@ export const MetadataViewProvider = ({
}); });
}, [updateFileTags, generateFileTagsAPI]); }, [updateFileTags, generateFileTagsAPI]);
const handleMoveRecord = (record) => { const areRecordsInSameFolder = useCallback((records) => {
const path = getParentDirFromRecord(record); if (!records || records.length <= 1) return true;
const currentRecordId = getRecordIdFromRecord(record); const firstPath = getParentDirFromRecord(records[0]);
const fileName = getFileNameFromRecord(record); return records.every(record => getParentDirFromRecord(record) === firstPath);
const dirent = new Dirent({ name: fileName }); }, []);
const callback = (...params) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); const calculateBatchMoveUpdateData = useCallback((records, targetRepoId, targetParentPath, sourceParentPath) => {
moveRecord && moveRecord(currentRecordId, ...params); 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) => { 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 unsubscribeUpdateFaceRecognition = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, updateFaceRecognition);
const unsubscribeUpdateDescription = eventBus.subscribe(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, updateRecordDescription); const unsubscribeUpdateDescription = eventBus.subscribe(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, updateRecordDescription);
const unsubscribeOCR = eventBus.subscribe(EVENT_BUS_TYPE.OCR, onOCR); 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); const unsubscribeSearchRows = eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_ROWS, handleSearchRows);
return () => { return () => {
@@ -669,6 +861,8 @@ export const MetadataViewProvider = ({
unsubscribeUpdateDescription(); unsubscribeUpdateDescription();
unsubscribeOCR(); unsubscribeOCR();
unsubscribeToggleMoveDialog(); unsubscribeToggleMoveDialog();
unsubscribeToggleCopyDialog();
unsubscribeDownloadRecords();
unsubscribeSearchRows(); unsubscribeSearchRows();
delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current); delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current);
}; };

View File

@@ -255,7 +255,9 @@ class DataProcessor {
this.updateSummaries(); this.updateSummaries();
break; 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 }); this.run(table, { collaborators });
break; break;
} }

View File

@@ -436,6 +436,23 @@ class Store {
this.applyOperation(operation); 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 }) { duplicateRecord(row_id, target_repo_id, dirent, target_parent_path, source_parent_path, { success_callback, fail_callback }) {
const type = OPERATION_TYPE.DUPLICATE_RECORD; const type = OPERATION_TYPE.DUPLICATE_RECORD;
const operation = this.createOperation({ const operation = this.createOperation({
@@ -452,6 +469,22 @@ class Store {
this.applyOperation(operation); 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 = []) { modifyFilters(filterConjunction, filters, basicFilters = []) {
const type = OPERATION_TYPE.MODIFY_FILTERS; const type = OPERATION_TYPE.MODIFY_FILTERS;
const operation = this.createOperation({ const operation = this.createOperation({

View File

@@ -128,7 +128,8 @@ export default function apply(data, operation) {
data.rows = updatedRows; data.rows = updatedRows;
return data; return data;
} }
case OPERATION_TYPE.MOVE_RECORD: { case OPERATION_TYPE.MOVE_RECORD:
case OPERATION_TYPE.MOVE_RECORDS: {
const { update_data } = operation; const { update_data } = operation;
const { const {
modify_row_ids: updateRowIds, modify_row_ids: updateRowIds,
@@ -145,6 +146,11 @@ export default function apply(data, operation) {
} }
return data; 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: { case OPERATION_TYPE.MODIFY_FILTERS: {
const { filter_conjunction, filters, basic_filters } = operation; const { filter_conjunction, filters, basic_filters } = operation;
data.view.filter_conjunction = filter_conjunction; 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_RECORD_VIA_BUTTON: 'modify_record_via_button',
MODIFY_LOCAL_RECORD: 'modify_local_record', MODIFY_LOCAL_RECORD: 'modify_local_record',
MOVE_RECORD: 'move_record', MOVE_RECORD: 'move_record',
MOVE_RECORDS: 'move_records',
DUPLICATE_RECORD: 'duplicate_record', DUPLICATE_RECORD: 'duplicate_record',
DUPLICATE_RECORDS: 'duplicate_records',
// face table // face table
RENAME_PEOPLE_NAME: 'rename_people_name', RENAME_PEOPLE_NAME: 'rename_people_name',
@@ -54,6 +56,8 @@ export const OPERATION_ATTRIBUTES = {
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'], [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.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.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_LOCAL_RECORD]: ['repo_id', 'row_id', 'parent_dir', 'file_name', 'updates'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters', 'basic_filters'], [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.MODIFY_SORTS,
OPERATION_TYPE.MOVE_RECORD, OPERATION_TYPE.MOVE_RECORD,
OPERATION_TYPE.DUPLICATE_RECORD, OPERATION_TYPE.DUPLICATE_RECORD,
OPERATION_TYPE.MOVE_RECORDS,
OPERATION_TYPE.DUPLICATE_RECORDS,
]; ];
export const VIEW_OPERATION = [ export const VIEW_OPERATION = [

View File

@@ -98,6 +98,20 @@ class ServerOperator {
}); });
break; 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: { case OPERATION_TYPE.DUPLICATE_RECORD: {
const { row_id, repo_id, target_repo_id, dirent, target_parent_path, source_parent_path } = operation; 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 => { seafileAPI.copyDir(repo_id, target_repo_id, target_parent_path, source_parent_path, dirent.name).then(res => {
@@ -110,6 +124,20 @@ class ServerOperator {
}); });
break; 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: { case OPERATION_TYPE.INSERT_COLUMN: {
const { repo_id, name, column_type, column_key, data } = operation; const { repo_id, name, column_type, column_key, data } = operation;
window.sfMetadataContext.insertColumn(repo_id, name, column_type, { key: column_key, data }).then(res => { 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', FILE_DETAILS: 'file-details',
DETECT_FACES: 'detect-faces', DETECT_FACES: 'detect-faces',
MOVE: 'move', MOVE: 'move',
COPY: 'copy',
DOWNLOAD: 'download',
}; };
const ContextMenu = ({ const ContextMenu = ({
@@ -119,7 +121,6 @@ const ContextMenu = ({
return list; return list;
} }
// handle selected records
const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : []; const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : [];
if (selectedRecordsIds.length > 1) { if (selectedRecordsIds.length > 1) {
let records = []; 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); const ableDeleteRecords = getAbleDeleteRecords(records);
if (ableDeleteRecords.length > 0) { if (ableDeleteRecords.length > 0) {
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords }); list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords });
@@ -177,6 +192,10 @@ const ContextMenu = ({
if (canModifyRow) { if (canModifyRow) {
modifyOptions.push({ value: OPERATION.MOVE, label: isFolder ? gettext('Move folder') : gettext('Move file'), record }); 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) { if (canDeleteRow) {
modifyOptions.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record }); modifyOptions.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record });
} }
@@ -301,9 +320,33 @@ const ContextMenu = ({
break; break;
} }
case OPERATION.MOVE: { case OPERATION.MOVE: {
const { record } = option; const { record, records } = option;
if (!record) break; if (records) {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, record); 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; break;
} }
default: { default: {

View File

@@ -1305,15 +1305,23 @@ class LibContentView extends React.Component {
}; };
// list operations // list operations
moveItemsAjaxCallback = (repoID, targetRepo, dirent, moveToDirentPath, nodeParentPath, taskId, byDialog = false) => { moveItemsAjaxCallback = (repoID, targetRepo, dirent, moveToDirentPath, nodeParentPath, taskId, byDialog = false, options = {}) => {
this.updateCurrentNotExistDirent(dirent); this.updateCurrentNotExistDirent(dirent);
const {
isBatchOperation = false,
batchFileNames = [],
customMessage = null
} = options;
const dirName = dirent.name; const dirName = dirent.name;
const direntPath = Utils.joinPath(nodeParentPath, dirName); const direntPath = Utils.joinPath(nodeParentPath, dirName);
if (repoID !== targetRepo.repo_id) { if (repoID !== targetRepo.repo_id) {
const operatedFilesLength = isBatchOperation ? batchFileNames.length : 1;
this.setState({ this.setState({
asyncCopyMoveTaskId: taskId, asyncCopyMoveTaskId: taskId,
asyncOperatedFilesLength: 1, asyncOperatedFilesLength: operatedFilesLength,
asyncOperationProgress: 0, asyncOperationProgress: 0,
asyncOperationType: 'move', asyncOperationType: 'move',
isCopyMoveProgressDialogShow: true, 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) { if (this.state.isTreePanelShown) {
this.deleteTreeNode(direntPath, updateAfterMove); if (isBatchOperation) {
} else { const direntPaths = batchFileNames.map(fileName => Utils.joinPath(nodeParentPath, fileName));
updateAfterMove(); 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 let message;
if (repoID === targetRepo.repo_id) {
let message = gettext('Successfully moved {name}.'); 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); message = message.replace('{name}', dirName);
toaster.success(message);
} }
toaster.success(message);
if (byDialog) { if (byDialog) {
this.updateRecentlyUsedList(targetRepo, moveToDirentPath); 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([]); this.onSelectedDirentListUpdate([]);
const {
isBatchOperation = false,
batchFileNames = [],
customMessage = null
} = options;
if (repoID !== targetRepo.repo_id) { if (repoID !== targetRepo.repo_id) {
const operatedFilesLength = isBatchOperation ? batchFileNames.length : 1;
this.setState({ this.setState({
asyncCopyMoveTaskId: taskId, asyncCopyMoveTaskId: taskId,
asyncOperatedFilesLength: 1, asyncOperatedFilesLength: operatedFilesLength,
asyncOperationProgress: 0, asyncOperationProgress: 0,
asyncOperationType: 'copy', asyncOperationType: 'copy',
isCopyMoveProgressDialogShow: true isCopyMoveProgressDialogShow: true
@@ -1409,8 +1435,19 @@ class LibContentView extends React.Component {
} }
const dirName = dirent.name; const dirName = dirent.name;
let message = gettext('Successfully copied %(name)s.'); let message;
message = message.replace('%(name)s', dirName);
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); toaster.success(message);
if (byDialog) { if (byDialog) {
@@ -2068,9 +2105,11 @@ class LibContentView extends React.Component {
}); });
}; };
deleteTreeNodes = (paths) => { deleteTreeNodes = (paths, callback) => {
let tree = treeHelper.deleteNodeListByPaths(this.state.treeData, paths); let tree = treeHelper.deleteNodeListByPaths(this.state.treeData, paths);
this.setState({ treeData: tree }); this.setState({ treeData: tree }, () => {
callback && callback();
});
}; };
moveTreeNode = (nodePath, moveToPath, moveToRepo, nodeName) => { moveTreeNode = (nodePath, moveToPath, moveToRepo, nodeName) => {