mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +00:00
Optimize/metadata context menu (#7206)
* move record * add copy option in gallery * optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimzie code --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
parent
6d17ce3093
commit
b27a0170c7
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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)) {
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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 = [
|
||||
|
@ -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 => {
|
||||
|
17
frontend/src/metadata/utils/cell/column/file-name.js
Normal file
17
frontend/src/metadata/utils/cell/column/file-name.js
Normal file
@ -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}`;
|
||||
};
|
@ -31,3 +31,7 @@ export {
|
||||
export {
|
||||
getTagsDisplayString,
|
||||
} from './tag';
|
||||
export {
|
||||
checkDuplicatedName,
|
||||
getUniqueFileName,
|
||||
} from './file-name';
|
||||
|
@ -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
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{isCopyDialogOpen && (
|
||||
<ModalPortal>
|
||||
<CopyDirent
|
||||
path={path}
|
||||
repoID={repoID}
|
||||
dirent={dirent}
|
||||
isMultipleOperation={false}
|
||||
repoEncrypted={false}
|
||||
onItemCopy={handleDuplicate}
|
||||
onCancelCopy={toggleCopyDialog}
|
||||
onAddFolder={addFolder}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -98,6 +135,8 @@ GalleryContextMenu.propTypes = {
|
||||
selectedImages: PropTypes.array,
|
||||
boundaryCoordinates: PropTypes.object,
|
||||
onDelete: PropTypes.func,
|
||||
onDuplicate: PropTypes.func,
|
||||
addFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default GalleryContextMenu;
|
||||
|
@ -10,7 +10,7 @@ import './index.css';
|
||||
const Gallery = () => {
|
||||
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
const { metadata, store, deleteRecords } = useMetadataView();
|
||||
const { metadata, store, deleteRecords, duplicateRecord, addFolder } = useMetadataView();
|
||||
|
||||
const onLoadMore = useCallback(async () => {
|
||||
if (isLoadingMore) return;
|
||||
@ -43,7 +43,14 @@ const Gallery = () => {
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-container">
|
||||
<Main isLoadingMore={isLoadingMore} metadata={metadata} onDelete={handleDelete} onLoadMore={onLoadMore} />
|
||||
<Main
|
||||
isLoadingMore={isLoadingMore}
|
||||
metadata={metadata}
|
||||
onDelete={handleDelete}
|
||||
onLoadMore={onLoadMore}
|
||||
duplicateRecord={duplicateRecord}
|
||||
onAddFolder={addFolder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,9 +6,9 @@ import ImageDialog from '../../../components/dialog/image-dialog';
|
||||
import ModalPortal from '../../../components/modal-portal';
|
||||
import { useMetadataView } from '../../hooks/metadata-view';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
|
||||
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
|
||||
import { siteRoot, fileServerRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
|
||||
import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
|
||||
import { getRowById } from '../../utils/table';
|
||||
import { getEventClassName } from '../../utils/common';
|
||||
import GalleryContextmenu from './context-menu';
|
||||
@ -17,7 +17,7 @@ import './index.css';
|
||||
|
||||
const OVER_SCAN_ROWS = 20;
|
||||
|
||||
const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onAddFolder }) => {
|
||||
const [isFirstLoading, setFirstLoading] = useState(true);
|
||||
const [zoomGear, setZoomGear] = useState(0);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
@ -65,7 +65,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
const firstSort = metadata.view.sorts[0];
|
||||
let init = metadata.rows.filter(row => Utils.imageCheck(getFileNameFromRecord(row)))
|
||||
.reduce((_init, record) => {
|
||||
const id = record[PRIVATE_COLUMN_KEY.ID];
|
||||
const id = getRecordIdFromRecord(record);
|
||||
const fileName = getFileNameFromRecord(record);
|
||||
const parentDir = getParentDirFromRecord(record);
|
||||
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||
@ -368,6 +368,8 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
selectedImages={selectedImages}
|
||||
boundaryCoordinates={containerRef?.current?.getBoundingClientRect() || {}}
|
||||
onDelete={handleDeleteSelectedImages}
|
||||
onDuplicate={duplicateRecord}
|
||||
addFolder={onAddFolder}
|
||||
/>
|
||||
{isImagePopupOpen && (
|
||||
<ModalPortal>
|
||||
@ -389,7 +391,9 @@ Main.propTypes = {
|
||||
isLoadingMore: PropTypes.bool,
|
||||
metadata: PropTypes.object,
|
||||
onDelete: PropTypes.func,
|
||||
onLoadMore: PropTypes.func
|
||||
onLoadMore: PropTypes.func,
|
||||
duplicateRecord: PropTypes.func,
|
||||
onAddFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Main;
|
||||
|
@ -13,6 +13,9 @@ import { getFileNameFromRecord, getParentDirFromRecord, getFileObjIdFromRecord,
|
||||
} from '../../../utils/cell';
|
||||
import FileTagsDialog from '../../../components/dialog/file-tags-dialog';
|
||||
import { openInNewTab, openParentFolder } from '../../../utils/file';
|
||||
import DeleteFolderDialog from '../../../../components/dialog/delete-folder-dialog';
|
||||
import MoveDirent from '../../../../components/dialog/move-dirent-dialog';
|
||||
import { Dirent } from '../../../../models';
|
||||
|
||||
const OPERATION = {
|
||||
CLEAR_SELECTED: 'clear-selected',
|
||||
@ -28,21 +31,27 @@ const OPERATION = {
|
||||
RENAME_FILE: 'rename-file',
|
||||
FILE_DETAIL: 'file-detail',
|
||||
FILE_DETAILS: 'file-details',
|
||||
MOVE: 'move',
|
||||
};
|
||||
|
||||
const ContextMenu = (props) => {
|
||||
const {
|
||||
isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected, updateRecords,
|
||||
getTableContentRect, getTableCanvasContainerRect, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileTags,
|
||||
} = props;
|
||||
const ContextMenu = ({
|
||||
isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected, updateRecords,
|
||||
getTableContentRect, getTableCanvasContainerRect, deleteRecords, selectNone, updateFileTags, moveRecord, addFolder
|
||||
}) => {
|
||||
const menuRef = useRef(null);
|
||||
const currentRecord = useRef(null);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const [fileTagsRecord, setFileTagsRecord] = useState(null);
|
||||
const [deletedFolderPath, setDeletedFolderPath] = useState('');
|
||||
const [isMoveDialogShow, setMoveDialogShow] = useState(false);
|
||||
|
||||
const { metadata } = useMetadataView();
|
||||
const { enableOCR } = useMetadataStatus();
|
||||
|
||||
const repoID = window.sfMetadataStore.repoId;
|
||||
|
||||
const checkCanModifyRow = (row) => {
|
||||
return window.sfMetadataContext.canModifyRow(row);
|
||||
};
|
||||
@ -56,6 +65,29 @@ const ContextMenu = (props) => {
|
||||
return records.filter(record => window.sfMetadataContext.checkCanDeleteRow(record));
|
||||
}, []);
|
||||
|
||||
const toggleDeleteFolderDialog = useCallback((record) => {
|
||||
if (deletedFolderPath) {
|
||||
currentRecord.current = null;
|
||||
setDeletedFolderPath('');
|
||||
return;
|
||||
}
|
||||
const parentDir = getParentDirFromRecord(record);
|
||||
const fileName = getFileNameFromRecord(record);
|
||||
currentRecord.current = record;
|
||||
setDeletedFolderPath(Utils.joinPath(parentDir, fileName));
|
||||
}, [deletedFolderPath]);
|
||||
|
||||
const toggleMoveDialog = useCallback((record) => {
|
||||
currentRecord.current = record || null;
|
||||
setMoveDialogShow(!isMoveDialogShow);
|
||||
}, [isMoveDialogShow]);
|
||||
|
||||
const deleteFolder = useCallback(() => {
|
||||
if (!currentRecord.current) return;
|
||||
const currentRecordId = getRecordIdFromRecord(currentRecord.current);
|
||||
deleteRecords([currentRecordId]);
|
||||
}, [deleteRecords]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!visible) return [];
|
||||
const permission = window.sfMetadataContext.getPermission();
|
||||
@ -162,6 +194,10 @@ const ContextMenu = (props) => {
|
||||
list.push({ value: OPERATION.RENAME_FILE, label: isFolder ? gettext('Rename folder') : gettext('Rename file'), record });
|
||||
}
|
||||
|
||||
if (canModifyRow) {
|
||||
list.push({ value: OPERATION.MOVE, label: isFolder ? gettext('Move folder') : gettext('Move file'), record });
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [visible, isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex, checkIsDescribableDoc, enableOCR, getAbleDeleteRecords]);
|
||||
|
||||
@ -300,7 +336,6 @@ const ContextMenu = (props) => {
|
||||
|
||||
const handleOptionClick = useCallback((event, option) => {
|
||||
event.stopPropagation();
|
||||
const repoID = window.sfMetadataStore.repoId;
|
||||
switch (option.value) {
|
||||
case OPERATION.OPEN_IN_NEW_TAB: {
|
||||
const { record } = option;
|
||||
@ -384,12 +419,18 @@ const ContextMenu = (props) => {
|
||||
updateFileDetails([record]);
|
||||
break;
|
||||
}
|
||||
case OPERATION.MOVE: {
|
||||
const { record } = option;
|
||||
if (!record) break;
|
||||
toggleMoveDialog(record);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
}, [onCopySelected, onClearSelected, generateDescription, imageCaption, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord]);
|
||||
}, [repoID, onCopySelected, onClearSelected, generateDescription, imageCaption, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord, toggleMoveDialog]);
|
||||
|
||||
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
@ -473,14 +514,35 @@ const ContextMenu = (props) => {
|
||||
);
|
||||
}, [visible, options, position, handleOptionClick]);
|
||||
|
||||
const currentRecordId = getRecordIdFromRecord(currentRecord.current);
|
||||
const fileName = getFileNameFromRecord(currentRecord.current);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderMenu()}
|
||||
{fileTagsRecord && (
|
||||
<FileTagsDialog record={fileTagsRecord} onToggle={toggleFileTagsRecord} onSubmit={updateFileTags} />
|
||||
)}
|
||||
{deletedFolderPath && (
|
||||
<DeleteFolderDialog
|
||||
repoID={repoID}
|
||||
path={deletedFolderPath}
|
||||
deleteFolder={deleteFolder}
|
||||
toggleDialog={toggleDeleteFolderDialog}
|
||||
/>
|
||||
)}
|
||||
{isMoveDialogShow && (
|
||||
<MoveDirent
|
||||
path={getParentDirFromRecord(currentRecord.current)}
|
||||
repoID={repoID}
|
||||
dirent={new Dirent({ name: fileName })}
|
||||
isMultipleOperation={false}
|
||||
onItemMove={(...params) => moveRecord(currentRecordId, ...params)}
|
||||
onCancelMove={toggleMoveDialog}
|
||||
onAddFolder={addFolder}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@ -493,6 +555,8 @@ ContextMenu.propTypes = {
|
||||
getTableContentRect: PropTypes.func,
|
||||
recordGetterByIndex: PropTypes.func,
|
||||
deleteRecords: PropTypes.func,
|
||||
moveRecord: PropTypes.func,
|
||||
addFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
|
@ -25,7 +25,9 @@ const Table = () => {
|
||||
modifyColumnOrder,
|
||||
modifyColumnWidth,
|
||||
insertColumn,
|
||||
updateFileTags
|
||||
updateFileTags,
|
||||
moveRecord,
|
||||
addFolder
|
||||
} = useMetadataView();
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@ -172,6 +174,8 @@ const Table = () => {
|
||||
updateFileTags={updateFileTags}
|
||||
onGridKeyDown={onHotKey}
|
||||
onGridKeyUp={onHotKeyUp}
|
||||
moveRecord={moveRecord}
|
||||
addFolder={addFolder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,6 @@ 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';
|
||||
@ -11,7 +10,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, Utils } from '../../../../../utils/utils';
|
||||
import { isMobile } from '../../../../../utils/utils';
|
||||
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import RecordMetrics from '../../utils/record-metrics';
|
||||
@ -43,7 +42,6 @@ class Records extends Component {
|
||||
},
|
||||
selectedPosition: this.initPosition,
|
||||
...initHorizontalScrollState,
|
||||
deletedFolderPath: '',
|
||||
};
|
||||
this.isWindows = isWindowsBrowser();
|
||||
this.isWebkit = isWebkitBrowser();
|
||||
@ -623,23 +621,6 @@ 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 } = this.props;
|
||||
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
@ -651,10 +632,11 @@ class Records extends Component {
|
||||
contextMenu: (
|
||||
<ContextMenu
|
||||
isGroupView={isGroupView}
|
||||
toggleDeleteFolderDialog={this.toggleDeleteFolderDialog}
|
||||
recordGetterByIndex={this.props.recordGetterByIndex}
|
||||
updateRecords={this.props.updateRecords}
|
||||
deleteRecords={this.props.deleteRecords}
|
||||
moveRecord={this.props.moveRecord}
|
||||
addFolder={this.props.addFolder}
|
||||
/>
|
||||
),
|
||||
hasSelectedRecord: this.hasSelectedRecord(),
|
||||
@ -694,7 +676,7 @@ class Records extends Component {
|
||||
render() {
|
||||
const {
|
||||
recordIds, recordsCount, table, isGroupView, groupOffsetLeft, renameColumn, modifyColumnData,
|
||||
deleteColumn, modifyColumnOrder, insertColumn,
|
||||
deleteColumn, modifyColumnOrder, insertColumn
|
||||
} = this.props;
|
||||
const { recordMetrics, columnMetrics, selectedRange, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
const { columns, totalWidth, lastFrozenColumnKey } = columnMetrics;
|
||||
@ -763,14 +745,6 @@ class Records extends Component {
|
||||
getRecordsSummaries={() => { }}
|
||||
loadAll={this.props.loadAll}
|
||||
/>
|
||||
{this.state.deletedFolderPath && (
|
||||
<DeleteFolderDialog
|
||||
repoID={window.sfMetadataStore.repoId}
|
||||
path={this.state.deletedFolderPath}
|
||||
deleteFolder={this.deleteFolder}
|
||||
toggleDialog={this.toggleDeleteFolderDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -803,6 +777,8 @@ Records.propTypes = {
|
||||
modifyColumnWidth: PropTypes.func,
|
||||
modifyColumnOrder: PropTypes.func,
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
moveRecord: PropTypes.func,
|
||||
addFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Records;
|
||||
|
@ -1228,55 +1228,59 @@ class LibContentView extends React.Component {
|
||||
};
|
||||
|
||||
// list operations
|
||||
onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath, byDialog = false) => {
|
||||
moveItemsAjaxCallback = (repoID, targetRepo, dirent, moveToDirentPath, nodeParentPath, taskId, byDialog = false) => {
|
||||
this.updateCurrentNotExistDirent(dirent);
|
||||
|
||||
const dirName = dirent.name;
|
||||
const direntPath = Utils.joinPath(nodeParentPath, dirName);
|
||||
if (repoID !== targetRepo.repo_id) {
|
||||
this.setState({
|
||||
asyncCopyMoveTaskId: taskId,
|
||||
asyncOperatedFilesLength: 1,
|
||||
asyncOperationProgress: 0,
|
||||
asyncOperationType: 'move',
|
||||
isCopyMoveProgressDialogShow: true,
|
||||
}, () => {
|
||||
this.currentMoveItemName = dirName;
|
||||
this.currentMoveItemPath = direntPath;
|
||||
this.getAsyncCopyMoveProgress(dirName, direntPath);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.deleteTreeNode(direntPath);
|
||||
}
|
||||
|
||||
// 1. move to current repo
|
||||
// 2. tow columns mode need update left tree
|
||||
if (repoID === targetRepo.repo_id && this.state.isTreePanelShown) {
|
||||
this.updateMoveCopyTreeNode(moveToDirentPath);
|
||||
}
|
||||
|
||||
this.moveDirent(direntPath, moveToDirentPath);
|
||||
|
||||
// show tip message if move to current repo
|
||||
if (repoID === targetRepo.repo_id) {
|
||||
let message = gettext('Successfully moved {name}.');
|
||||
message = message.replace('{name}', dirName);
|
||||
toaster.success(message);
|
||||
}
|
||||
|
||||
if (byDialog) {
|
||||
this.updateRecentlyUsedRepos(targetRepo, moveToDirentPath);
|
||||
}
|
||||
};
|
||||
|
||||
onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath, byDialog = false) => {
|
||||
let repoID = this.props.repoID;
|
||||
// just for view list state
|
||||
let dirName = dirent.name;
|
||||
if (!nodeParentPath) {
|
||||
nodeParentPath = this.state.path;
|
||||
}
|
||||
let direntPath = Utils.joinPath(nodeParentPath, dirName);
|
||||
|
||||
seafileAPI.moveDir(repoID, destRepo.repo_id, moveToDirentPath, nodeParentPath, dirName).then(res => {
|
||||
if (repoID !== destRepo.repo_id) {
|
||||
this.setState({
|
||||
asyncCopyMoveTaskId: res.data.task_id,
|
||||
asyncOperatedFilesLength: 1,
|
||||
asyncOperationProgress: 0,
|
||||
asyncOperationType: 'move',
|
||||
isCopyMoveProgressDialogShow: true,
|
||||
}, () => {
|
||||
this.currentMoveItemName = dirName;
|
||||
this.currentMoveItemPath = direntPath;
|
||||
this.getAsyncCopyMoveProgress(dirName, direntPath);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.deleteTreeNode(direntPath);
|
||||
}
|
||||
|
||||
// 1. move to current repo
|
||||
// 2. tow columns mode need update left tree
|
||||
if (repoID === destRepo.repo_id &&
|
||||
this.state.isTreePanelShown) {
|
||||
this.updateMoveCopyTreeNode(moveToDirentPath);
|
||||
}
|
||||
|
||||
this.moveDirent(direntPath, moveToDirentPath);
|
||||
|
||||
// show tip message if move to current repo
|
||||
if (repoID === destRepo.repo_id) {
|
||||
let message = gettext('Successfully moved {name}.');
|
||||
message = message.replace('{name}', dirName);
|
||||
toaster.success(message);
|
||||
}
|
||||
|
||||
if (byDialog) {
|
||||
this.updateRecentlyUsedRepos(destRepo, moveToDirentPath);
|
||||
}
|
||||
|
||||
this.moveItemsAjaxCallback(repoID, destRepo, dirent, moveToDirentPath, nodeParentPath, res.data.task_id, byDialog);
|
||||
}).catch((error) => {
|
||||
if (!error.response.data.lib_need_decrypt) {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
@ -1285,19 +1289,51 @@ class LibContentView extends React.Component {
|
||||
errMessage = errMessage.replace('{name}', dirName);
|
||||
}
|
||||
toaster.danger(errMessage);
|
||||
} else {
|
||||
this.setState({
|
||||
libNeedDecryptWhenMove: true,
|
||||
destRepoWhenCopyMove: destRepo,
|
||||
destDirentPathWhenCopyMove: moveToDirentPath,
|
||||
copyMoveSingleItem: true,
|
||||
srcDirentWhenCopyMove: dirent,
|
||||
srcNodeParentPathWhenCopyMove: nodeParentPath,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
libNeedDecryptWhenMove: true,
|
||||
destRepoWhenCopyMove: destRepo,
|
||||
destDirentPathWhenCopyMove: moveToDirentPath,
|
||||
copyMoveSingleItem: true,
|
||||
srcDirentWhenCopyMove: dirent,
|
||||
srcNodeParentPathWhenCopyMove: nodeParentPath,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
copyItemsAjaxCallback = (repoID, targetRepo, dirent, copyToDirentPath, nodeParentPath, taskId, byDialog = false) => {
|
||||
if (repoID !== targetRepo.repo_id) {
|
||||
this.setState({
|
||||
asyncCopyMoveTaskId: taskId,
|
||||
asyncOperatedFilesLength: 1,
|
||||
asyncOperationProgress: 0,
|
||||
asyncOperationType: 'copy',
|
||||
isCopyMoveProgressDialogShow: true
|
||||
}, () => {
|
||||
this.getAsyncCopyMoveProgress();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.updateMoveCopyTreeNode(copyToDirentPath);
|
||||
}
|
||||
|
||||
if (copyToDirentPath === nodeParentPath && this.state.currentMode !== METADATA_MODE) {
|
||||
this.loadDirentList(this.state.path);
|
||||
}
|
||||
|
||||
const dirName = dirent.name;
|
||||
let message = gettext('Successfully copied %(name)s.');
|
||||
message = message.replace('%(name)s', dirName);
|
||||
toaster.success(message);
|
||||
|
||||
if (byDialog) {
|
||||
this.updateRecentlyUsedRepos(targetRepo, copyToDirentPath);
|
||||
}
|
||||
};
|
||||
|
||||
onCopyItem = (destRepo, dirent, copyToDirentPath, nodeParentPath, byDialog = false) => {
|
||||
let repoID = this.props.repoID;
|
||||
// just for view list state
|
||||
@ -1307,36 +1343,7 @@ class LibContentView extends React.Component {
|
||||
}
|
||||
|
||||
seafileAPI.copyDir(repoID, destRepo.repo_id, copyToDirentPath, nodeParentPath, dirName).then(res => {
|
||||
|
||||
if (repoID !== destRepo.repo_id) {
|
||||
this.setState({
|
||||
asyncCopyMoveTaskId: res.data.task_id,
|
||||
asyncOperatedFilesLength: 1,
|
||||
asyncOperationProgress: 0,
|
||||
asyncOperationType: 'copy',
|
||||
isCopyMoveProgressDialogShow: true
|
||||
}, () => {
|
||||
this.getAsyncCopyMoveProgress();
|
||||
});
|
||||
}
|
||||
|
||||
if (repoID === destRepo.repo_id) {
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.updateMoveCopyTreeNode(copyToDirentPath);
|
||||
}
|
||||
|
||||
if (copyToDirentPath === nodeParentPath) {
|
||||
this.loadDirentList(this.state.path);
|
||||
}
|
||||
|
||||
let message = gettext('Successfully copied %(name)s.');
|
||||
message = message.replace('%(name)s', dirName);
|
||||
toaster.success(message);
|
||||
|
||||
if (byDialog) {
|
||||
this.updateRecentlyUsedRepos(destRepo, copyToDirentPath);
|
||||
}
|
||||
}
|
||||
this.copyItemsAjaxCallback(repoID, destRepo, dirent, copyToDirentPath, nodeParentPath, res.data.task_id || null, byDialog);
|
||||
}).catch((error) => {
|
||||
if (!error.response.data.lib_need_decrypt) {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
@ -2348,7 +2355,9 @@ class LibContentView extends React.Component {
|
||||
deleteFilesCallback={this.deleteItemsAjaxCallback}
|
||||
renameFileCallback={this.renameItemAjaxCallback}
|
||||
onItemMove={this.onMoveItem}
|
||||
moveFileCallback={this.moveItemsAjaxCallback}
|
||||
onItemCopy={this.onCopyItem}
|
||||
copyFileCallback={this.copyItemsAjaxCallback}
|
||||
onItemConvert={this.onConvertItem}
|
||||
onDirentClick={this.onDirentClick}
|
||||
updateDirent={this.updateDirent}
|
||||
|
Loading…
Reference in New Issue
Block a user