1
0
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:
Aries 2024-12-19 16:21:06 +08:00 committed by GitHub
parent 6d17ce3093
commit b27a0170c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 493 additions and 177 deletions

View File

@ -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}

View File

@ -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;

View File

@ -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}

View File

@ -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)) {

View File

@ -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) => {

View File

@ -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;

View File

@ -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 = [

View File

@ -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 => {

View 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}`;
};

View File

@ -31,3 +31,7 @@ export {
export {
getTagsDisplayString,
} from './tag';
export {
checkDuplicatedName,
getUniqueFileName,
} from './file-name';

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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>
);

View File

@ -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;

View File

@ -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}