2024-08-20 11:21:19 +08:00
|
|
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2024-08-23 17:51:52 +08:00
|
|
|
import toaster from '../../../../components/toast';
|
2024-09-14 16:31:32 +08:00
|
|
|
import { gettext, siteRoot } from '../../../../utils/constants';
|
|
|
|
import { Utils } from '../../../../utils/utils';
|
|
|
|
import { useMetadataView } from '../../../hooks/metadata-view';
|
2024-12-13 16:41:42 +08:00
|
|
|
import { useMetadataStatus } from '../../../../hooks';
|
2024-09-29 17:52:25 +08:00
|
|
|
import { getColumnByKey, isNameColumn } from '../../../utils/column';
|
|
|
|
import { checkIsDir } from '../../../utils/row';
|
|
|
|
import { EVENT_BUS_TYPE, EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../../constants';
|
2024-10-18 13:25:10 +08:00
|
|
|
import { getFileNameFromRecord, getParentDirFromRecord, getFileObjIdFromRecord,
|
2024-11-22 17:11:55 +08:00
|
|
|
getRecordIdFromRecord,
|
2024-10-18 13:25:10 +08:00
|
|
|
} from '../../../utils/cell';
|
2024-12-13 10:16:52 +08:00
|
|
|
import FileTagsDialog from '../../../components/dialog/file-tags-dialog';
|
2024-09-29 17:52:25 +08:00
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
const OPERATION = {
|
|
|
|
CLEAR_SELECTED: 'clear-selected',
|
|
|
|
COPY_SELECTED: 'copy-selected',
|
|
|
|
OPEN_PARENT_FOLDER: 'open-parent-folder',
|
|
|
|
OPEN_IN_NEW_TAB: 'open-new-tab',
|
2024-09-10 15:18:07 +08:00
|
|
|
GENERATE_DESCRIPTION: 'generate-description',
|
2024-12-13 16:41:42 +08:00
|
|
|
OCR: 'ocr',
|
2024-08-30 14:36:52 +08:00
|
|
|
IMAGE_CAPTION: 'image-caption',
|
2024-12-13 10:16:52 +08:00
|
|
|
FILE_TAGS: 'file-tags',
|
2024-09-29 17:52:25 +08:00
|
|
|
DELETE_RECORD: 'delete-record',
|
|
|
|
DELETE_RECORDS: 'delete-records',
|
|
|
|
RENAME_FILE: 'rename-file',
|
2024-10-18 13:25:10 +08:00
|
|
|
FILE_DETAIL: 'file-detail',
|
|
|
|
FILE_DETAILS: 'file-details',
|
2024-08-20 11:21:19 +08:00
|
|
|
};
|
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const ContextMenu = (props) => {
|
|
|
|
const {
|
|
|
|
isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected, updateRecords,
|
2024-12-02 16:42:22 +08:00
|
|
|
getTableContentRect, getTableCanvasContainerRect, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileTags,
|
2024-09-29 17:52:25 +08:00
|
|
|
} = props;
|
2024-08-20 11:21:19 +08:00
|
|
|
const menuRef = useRef(null);
|
|
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
2024-12-13 10:16:52 +08:00
|
|
|
const [fileTagsRecord, setFileTagsRecord] = useState(null);
|
2024-09-29 17:52:25 +08:00
|
|
|
|
2024-09-14 16:31:32 +08:00
|
|
|
const { metadata } = useMetadataView();
|
2024-12-13 16:41:42 +08:00
|
|
|
const { enableOCR } = useMetadataStatus();
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const checkCanModifyRow = (row) => {
|
|
|
|
return window.sfMetadataContext.canModifyRow(row);
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkIsDescribableDoc = useCallback((record) => {
|
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
return checkCanModifyRow(record) && Utils.isDescriptionSupportedFile(fileName);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const getAbleDeleteRecords = useCallback((records) => {
|
|
|
|
return records.filter(record => window.sfMetadataContext.checkCanDeleteRow(record));
|
|
|
|
}, []);
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
const options = useMemo(() => {
|
2024-08-23 17:51:52 +08:00
|
|
|
if (!visible) return [];
|
2024-08-20 11:21:19 +08:00
|
|
|
const permission = window.sfMetadataContext.getPermission();
|
|
|
|
const isReadonly = permission === 'r';
|
2024-08-23 17:51:52 +08:00
|
|
|
const { columns } = metadata;
|
2024-09-10 15:18:07 +08:00
|
|
|
const descriptionColumn = getColumnByKey(columns, PRIVATE_COLUMN_KEY.FILE_DESCRIPTION);
|
2024-12-02 16:42:22 +08:00
|
|
|
const tagsColumn = getColumnByKey(columns, PRIVATE_COLUMN_KEY.TAGS);
|
2024-08-20 11:21:19 +08:00
|
|
|
let list = [];
|
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
// handle selected multiple cells
|
2024-08-20 11:21:19 +08:00
|
|
|
if (selectedRange) {
|
|
|
|
!isReadonly && list.push({ value: OPERATION.CLEAR_SELECTED, label: gettext('Clear selected') });
|
|
|
|
list.push({ value: OPERATION.COPY_SELECTED, label: gettext('Copy selected') });
|
2024-09-29 17:52:25 +08:00
|
|
|
|
|
|
|
const { topLeft, bottomRight } = selectedRange;
|
|
|
|
let records = [];
|
|
|
|
for (let i = topLeft.rowIdx; i <= bottomRight.rowIdx; i++) {
|
|
|
|
const record = recordGetterByIndex({ isGroupView, groupRecordIndex: topLeft.groupRecordIndex, recordIndex: i });
|
|
|
|
if (record) {
|
|
|
|
records.push(record);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ableDeleteRecords = getAbleDeleteRecords(records);
|
|
|
|
if (ableDeleteRecords.length > 0) {
|
2024-10-16 14:08:47 +08:00
|
|
|
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete selected'), records: ableDeleteRecords });
|
2024-09-29 17:52:25 +08:00
|
|
|
}
|
|
|
|
|
2024-10-18 13:25:10 +08:00
|
|
|
const imageOrVideoRecords = records.filter(record => {
|
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
|
|
|
|
});
|
|
|
|
if (imageOrVideoRecords.length > 0) {
|
|
|
|
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
|
|
|
|
}
|
2024-08-20 11:21:19 +08:00
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
// handle selected records
|
|
|
|
const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : [];
|
|
|
|
if (selectedRecordsIds.length > 1) {
|
|
|
|
let records = [];
|
|
|
|
selectedRecordsIds.forEach(id => {
|
|
|
|
const record = metadata.id_row_map[id];
|
|
|
|
if (record) {
|
|
|
|
records.push(record);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const ableDeleteRecords = getAbleDeleteRecords(records);
|
|
|
|
if (ableDeleteRecords.length > 0) {
|
|
|
|
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords });
|
|
|
|
}
|
2024-10-18 13:25:10 +08:00
|
|
|
const imageOrVideoRecords = records.filter(record => {
|
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
|
|
|
|
});
|
|
|
|
if (imageOrVideoRecords.length > 0) {
|
|
|
|
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
|
|
|
|
}
|
2024-08-20 11:21:19 +08:00
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
// handle selected cell
|
2024-08-20 11:21:19 +08:00
|
|
|
if (!selectedPosition) return list;
|
2024-09-29 17:52:25 +08:00
|
|
|
const { groupRecordIndex, rowIdx: recordIndex, idx } = selectedPosition;
|
|
|
|
const column = columns[idx];
|
2024-08-20 11:21:19 +08:00
|
|
|
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
|
|
|
|
if (!record) return list;
|
2024-09-29 17:52:25 +08:00
|
|
|
|
|
|
|
const canModifyRow = checkCanModifyRow(record);
|
|
|
|
const canDeleteRow = window.sfMetadataContext.checkCanDeleteRow(record);
|
|
|
|
const isFolder = checkIsDir(record);
|
|
|
|
list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab'), record });
|
|
|
|
list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder'), record });
|
2024-10-18 13:25:10 +08:00
|
|
|
const fileName = getFileNameFromRecord(record);
|
2024-09-29 17:52:25 +08:00
|
|
|
|
2024-09-10 15:18:07 +08:00
|
|
|
if (descriptionColumn) {
|
2024-09-29 17:52:25 +08:00
|
|
|
if (checkIsDescribableDoc(record)) {
|
|
|
|
list.push({ value: OPERATION.GENERATE_DESCRIPTION, label: gettext('Generate description'), record });
|
2024-10-18 13:25:10 +08:00
|
|
|
} else if (canModifyRow && Utils.imageCheck(fileName)) {
|
2024-09-29 17:52:25 +08:00
|
|
|
list.push({ value: OPERATION.IMAGE_CAPTION, label: gettext('Generate image description'), record });
|
2024-08-23 17:51:52 +08:00
|
|
|
}
|
|
|
|
}
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-12-13 16:41:42 +08:00
|
|
|
if (enableOCR && canModifyRow && Utils.imageCheck(fileName)) {
|
|
|
|
list.push({ value: OPERATION.OCR, label: gettext('OCR'), record });
|
|
|
|
}
|
|
|
|
|
2024-10-18 13:25:10 +08:00
|
|
|
if (canModifyRow && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName))) {
|
|
|
|
list.push({ value: OPERATION.FILE_DETAIL, label: gettext('Extract file detail'), record: record });
|
|
|
|
}
|
|
|
|
|
2024-12-13 10:16:52 +08:00
|
|
|
if (tagsColumn && canModifyRow && (Utils.imageCheck(fileName) || checkIsDescribableDoc(record))) {
|
|
|
|
list.push({ value: OPERATION.FILE_TAGS, label: gettext('Generate file tags'), record: record });
|
2024-12-02 16:42:22 +08:00
|
|
|
}
|
|
|
|
|
2024-10-18 13:25:10 +08:00
|
|
|
// handle delete folder/file
|
2024-09-29 17:52:25 +08:00
|
|
|
if (canDeleteRow) {
|
2024-10-16 14:08:47 +08:00
|
|
|
list.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record });
|
2024-09-29 17:52:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (canModifyRow && column && isNameColumn(column)) {
|
2024-10-16 14:08:47 +08:00
|
|
|
list.push({ value: OPERATION.RENAME_FILE, label: isFolder ? gettext('Rename folder') : gettext('Rename file'), record });
|
2024-09-29 17:52:25 +08:00
|
|
|
}
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
return list;
|
2024-12-13 16:41:42 +08:00
|
|
|
}, [visible, isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex, checkIsDescribableDoc, enableOCR, getAbleDeleteRecords]);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
|
|
|
const handleHide = useCallback((event) => {
|
2024-09-29 17:52:25 +08:00
|
|
|
if (!menuRef.current && visible) {
|
|
|
|
setVisible(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
|
|
setVisible(false);
|
|
|
|
}
|
2024-09-29 17:52:25 +08:00
|
|
|
}, [menuRef, visible]);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const onOpenFileInNewTab = useCallback((record) => {
|
2024-08-20 11:21:19 +08:00
|
|
|
const repoID = window.sfMetadataStore.repoId;
|
2024-09-29 17:52:25 +08:00
|
|
|
const isFolder = checkIsDir(record);
|
|
|
|
const parentDir = getParentDirFromRecord(record);
|
|
|
|
const fileName = getFileNameFromRecord(record);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const url = isFolder ?
|
|
|
|
window.location.origin + window.location.pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)) :
|
|
|
|
`${siteRoot}lib/${repoID}/file${Utils.encodePath(Utils.joinPath(parentDir, fileName))}`;
|
2024-08-20 11:21:19 +08:00
|
|
|
|
|
|
|
window.open(url, '_blank');
|
2024-09-29 17:52:25 +08:00
|
|
|
}, []);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const onOpenParentFolder = useCallback((event, record) => {
|
2024-08-20 11:21:19 +08:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
2024-09-29 17:52:25 +08:00
|
|
|
let parentDir = getParentDirFromRecord(record);
|
2024-09-04 07:33:16 +08:00
|
|
|
|
|
|
|
if (window.location.pathname.endsWith('/')) {
|
|
|
|
parentDir = parentDir.slice(1);
|
|
|
|
}
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
const url = window.location.origin + window.location.pathname + Utils.encodePath(parentDir);
|
|
|
|
window.open(url, '_blank');
|
2024-09-29 17:52:25 +08:00
|
|
|
}, []);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const generateDescription = useCallback((record) => {
|
2024-09-10 15:18:07 +08:00
|
|
|
const descriptionColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION;
|
2024-08-30 14:36:52 +08:00
|
|
|
let path = '';
|
2024-08-23 17:51:52 +08:00
|
|
|
let idOldRecordData = {};
|
|
|
|
let idOriginalOldRecordData = {};
|
2024-09-29 17:52:25 +08:00
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
if (Utils.isDescriptionSupportedFile(fileName) && checkCanModifyRow(record)) {
|
|
|
|
const parentDir = getParentDirFromRecord(record);
|
2024-08-30 14:36:52 +08:00
|
|
|
path = Utils.joinPath(parentDir, fileName);
|
2024-09-10 15:18:07 +08:00
|
|
|
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [descriptionColumnKey]: record[descriptionColumnKey] };
|
|
|
|
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [descriptionColumnKey]: record[descriptionColumnKey] };
|
2024-08-23 17:51:52 +08:00
|
|
|
}
|
2024-08-30 14:36:52 +08:00
|
|
|
if (path === '') return;
|
2024-09-10 15:18:07 +08:00
|
|
|
window.sfMetadataContext.generateDescription(path).then(res => {
|
|
|
|
const description = res.data.summary;
|
2024-08-30 14:36:52 +08:00
|
|
|
const updateRecordId = record[PRIVATE_COLUMN_KEY.ID];
|
|
|
|
const recordIds = [updateRecordId];
|
2024-08-23 17:51:52 +08:00
|
|
|
let idRecordUpdates = {};
|
|
|
|
let idOriginalRecordUpdates = {};
|
2024-09-10 15:18:07 +08:00
|
|
|
idRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description };
|
|
|
|
idOriginalRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description };
|
2024-08-23 17:51:52 +08:00
|
|
|
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
|
|
|
|
}).catch(error => {
|
2024-09-10 15:18:07 +08:00
|
|
|
const errorMessage = gettext('Failed to generate description');
|
2024-08-23 17:51:52 +08:00
|
|
|
toaster.danger(errorMessage);
|
|
|
|
});
|
2024-09-29 17:52:25 +08:00
|
|
|
}, [updateRecords]);
|
2024-08-30 14:36:52 +08:00
|
|
|
|
2024-09-29 17:52:25 +08:00
|
|
|
const imageCaption = useCallback((record) => {
|
2024-09-10 15:18:07 +08:00
|
|
|
const summaryColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION;
|
2024-08-30 14:36:52 +08:00
|
|
|
let path = '';
|
|
|
|
let idOldRecordData = {};
|
|
|
|
let idOriginalOldRecordData = {};
|
2024-09-29 17:52:25 +08:00
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
if (Utils.imageCheck(fileName) && checkCanModifyRow(record)) {
|
|
|
|
const parentDir = getParentDirFromRecord(record);
|
2024-08-30 14:36:52 +08:00
|
|
|
path = Utils.joinPath(parentDir, fileName);
|
|
|
|
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
|
|
|
|
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
|
|
|
|
}
|
|
|
|
if (path === '') return;
|
|
|
|
window.sfMetadataContext.imageCaption(path).then(res => {
|
|
|
|
const desc = res.data.desc;
|
|
|
|
const updateRecordId = record[PRIVATE_COLUMN_KEY.ID];
|
|
|
|
const recordIds = [updateRecordId];
|
|
|
|
let idRecordUpdates = {};
|
|
|
|
let idOriginalRecordUpdates = {};
|
|
|
|
idRecordUpdates[updateRecordId] = { [summaryColumnKey]: desc };
|
|
|
|
idOriginalRecordUpdates[updateRecordId] = { [summaryColumnKey]: desc };
|
|
|
|
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
|
|
|
|
}).catch(error => {
|
|
|
|
const errorMessage = gettext('Failed to generate image description');
|
|
|
|
toaster.danger(errorMessage);
|
|
|
|
});
|
2024-09-29 17:52:25 +08:00
|
|
|
}, [updateRecords]);
|
2024-08-23 17:51:52 +08:00
|
|
|
|
2024-12-13 10:16:52 +08:00
|
|
|
const toggleFileTagsRecord = useCallback((record = null) => {
|
|
|
|
setFileTagsRecord(record);
|
2024-12-02 16:42:22 +08:00
|
|
|
}, []);
|
|
|
|
|
2024-12-13 16:41:42 +08:00
|
|
|
const ocr = useCallback((record) => {
|
|
|
|
const ocrResultColumnKey = PRIVATE_COLUMN_KEY.OCR;
|
|
|
|
let path = '';
|
|
|
|
let idOldRecordData = {};
|
|
|
|
let idOriginalOldRecordData = {};
|
|
|
|
const fileName = getFileNameFromRecord(record);
|
|
|
|
if (Utils.imageCheck(fileName) && checkCanModifyRow(record)) {
|
|
|
|
const parentDir = getParentDirFromRecord(record);
|
|
|
|
path = Utils.joinPath(parentDir, fileName);
|
|
|
|
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [ocrResultColumnKey]: record[ocrResultColumnKey] };
|
|
|
|
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [ocrResultColumnKey]: record[ocrResultColumnKey] };
|
|
|
|
}
|
|
|
|
if (path === '') return;
|
|
|
|
window.sfMetadataContext.ocr(path).then(res => {
|
|
|
|
const ocrResult = res.data.ocr_result;
|
|
|
|
const updateRecordId = record[PRIVATE_COLUMN_KEY.ID];
|
|
|
|
const recordIds = [updateRecordId];
|
|
|
|
let idRecordUpdates = {};
|
|
|
|
let idOriginalRecordUpdates = {};
|
|
|
|
idRecordUpdates[updateRecordId] = { [ocrResultColumnKey]: ocrResult ? JSON.stringify(ocrResult) : null };
|
|
|
|
idOriginalRecordUpdates[updateRecordId] = { [ocrResultColumnKey]: ocrResult ? JSON.stringify(ocrResult) : null };
|
|
|
|
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
|
|
|
|
}).catch(error => {
|
|
|
|
const errorMessage = gettext('OCR failed');
|
|
|
|
toaster.danger(errorMessage);
|
|
|
|
});
|
|
|
|
}, [updateRecords]);
|
|
|
|
|
2024-10-18 13:25:10 +08:00
|
|
|
const updateFileDetails = useCallback((records) => {
|
|
|
|
const recordObjIds = records.map(record => getFileObjIdFromRecord(record));
|
|
|
|
if (recordObjIds.length > 50) {
|
|
|
|
toaster.danger(gettext('Select up to 50 files'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-11-22 17:11:55 +08:00
|
|
|
const recordIds = records.map(record => getRecordIdFromRecord(record));
|
2024-10-18 13:25:10 +08:00
|
|
|
window.sfMetadataContext.extractFileDetails(recordObjIds).then(res => {
|
|
|
|
const captureColumn = getColumnByKey(metadata.columns, PRIVATE_COLUMN_KEY.CAPTURE_TIME);
|
|
|
|
|
|
|
|
if (captureColumn) {
|
|
|
|
let idOldRecordData = {};
|
|
|
|
let idOriginalOldRecordData = {};
|
|
|
|
const captureColumnKey = PRIVATE_COLUMN_KEY.CAPTURE_TIME;
|
|
|
|
records.forEach(record => {
|
|
|
|
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [captureColumnKey]: record[captureColumnKey] };
|
|
|
|
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [captureColumnKey]: record[captureColumnKey] };
|
|
|
|
});
|
|
|
|
let idRecordUpdates = {};
|
|
|
|
let idOriginalRecordUpdates = {};
|
|
|
|
res.data.details.forEach(detail => {
|
|
|
|
const updateRecordId = detail[PRIVATE_COLUMN_KEY.ID];
|
|
|
|
idRecordUpdates[updateRecordId] = { [captureColumnKey]: detail[captureColumnKey] };
|
|
|
|
idOriginalRecordUpdates[updateRecordId] = { [captureColumnKey]: detail[captureColumnKey] };
|
|
|
|
});
|
|
|
|
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
|
|
|
|
}
|
|
|
|
}).catch(error => {
|
|
|
|
const errorMessage = gettext('Failed to extract file details');
|
|
|
|
toaster.danger(errorMessage);
|
|
|
|
});
|
|
|
|
}, [metadata, updateRecords]);
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
const handleOptionClick = useCallback((event, option) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
switch (option.value) {
|
|
|
|
case OPERATION.OPEN_IN_NEW_TAB: {
|
2024-09-29 17:52:25 +08:00
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
|
|
|
onOpenFileInNewTab(record);
|
2024-08-20 11:21:19 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OPERATION.OPEN_PARENT_FOLDER: {
|
2024-09-29 17:52:25 +08:00
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
|
|
|
onOpenParentFolder(event, record);
|
2024-08-20 11:21:19 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OPERATION.COPY_SELECTED: {
|
|
|
|
onCopySelected && onCopySelected();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OPERATION.CLEAR_SELECTED: {
|
|
|
|
onClearSelected && onClearSelected();
|
|
|
|
break;
|
|
|
|
}
|
2024-09-10 15:18:07 +08:00
|
|
|
case OPERATION.GENERATE_DESCRIPTION: {
|
2024-09-29 17:52:25 +08:00
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
|
|
|
generateDescription(record);
|
2024-08-23 17:51:52 +08:00
|
|
|
break;
|
|
|
|
}
|
2024-08-30 14:36:52 +08:00
|
|
|
case OPERATION.IMAGE_CAPTION: {
|
2024-09-29 17:52:25 +08:00
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
|
|
|
imageCaption(record);
|
|
|
|
break;
|
|
|
|
}
|
2024-12-13 10:16:52 +08:00
|
|
|
case OPERATION.FILE_TAGS: {
|
2024-12-02 16:42:22 +08:00
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
2024-12-13 10:16:52 +08:00
|
|
|
toggleFileTagsRecord(record);
|
2024-12-02 16:42:22 +08:00
|
|
|
break;
|
|
|
|
}
|
2024-12-13 16:41:42 +08:00
|
|
|
case OPERATION.OCR: {
|
|
|
|
const { record } = option;
|
|
|
|
if (!record) break;
|
|
|
|
ocr(record);
|
|
|
|
break;
|
|
|
|
}
|
2024-09-29 17:52:25 +08:00
|
|
|
case OPERATION.DELETE_RECORD: {
|
|
|
|
const { record } = option;
|
|
|
|
if (!record || !record._id || !deleteRecords) break;
|
|
|
|
if (checkIsDir(record)) {
|
|
|
|
toggleDeleteFolderDialog(record);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
deleteRecords([record._id]);
|
2024-08-30 14:36:52 +08:00
|
|
|
break;
|
|
|
|
}
|
2024-09-29 17:52:25 +08:00
|
|
|
case OPERATION.DELETE_RECORDS: {
|
|
|
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
|
|
|
selectNone && selectNone();
|
|
|
|
|
|
|
|
const { records } = option;
|
|
|
|
const recordsIds = Array.isArray(records) ? records.map((record) => record._id).filter(Boolean) : [];
|
|
|
|
if (recordsIds.length === 0 || !deleteRecords) break;
|
|
|
|
deleteRecords(recordsIds);
|
2024-09-21 16:41:21 +08:00
|
|
|
break;
|
|
|
|
}
|
2024-09-29 17:52:25 +08:00
|
|
|
case OPERATION.RENAME_FILE: {
|
|
|
|
const { record } = option;
|
|
|
|
if (!record || !record._id) break;
|
|
|
|
|
|
|
|
// rename file via FileNameEditor
|
|
|
|
window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR);
|
2024-09-21 16:41:21 +08:00
|
|
|
break;
|
|
|
|
}
|
2024-10-18 13:25:10 +08:00
|
|
|
case OPERATION.FILE_DETAILS: {
|
|
|
|
const { records } = option;
|
|
|
|
updateFileDetails(records);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OPERATION.FILE_DETAIL: {
|
|
|
|
const { record } = option;
|
|
|
|
updateFileDetails([record]);
|
|
|
|
break;
|
|
|
|
}
|
2024-08-20 11:21:19 +08:00
|
|
|
default: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setVisible(false);
|
2024-12-13 16:41:42 +08:00
|
|
|
}, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord]);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-08-26 17:35:36 +08:00
|
|
|
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
2024-08-20 11:21:19 +08:00
|
|
|
let menuStyles = {
|
|
|
|
top: y,
|
|
|
|
left: x
|
|
|
|
};
|
|
|
|
if (!menuRef.current) return menuStyles;
|
|
|
|
const rect = menuRef.current.getBoundingClientRect();
|
2024-08-26 17:35:36 +08:00
|
|
|
const tableCanvasContainerRect = getTableCanvasContainerRect();
|
|
|
|
const tableContentRect = getTableContentRect();
|
|
|
|
const { right: innerWidth, bottom: innerHeight } = tableContentRect;
|
|
|
|
menuStyles.top = menuStyles.top - tableCanvasContainerRect.top;
|
|
|
|
menuStyles.left = menuStyles.left - tableCanvasContainerRect.left;
|
2024-08-20 11:21:19 +08:00
|
|
|
|
2024-08-26 17:35:36 +08:00
|
|
|
if (y + rect.height > innerHeight - 10) {
|
2024-08-20 11:21:19 +08:00
|
|
|
menuStyles.top -= rect.height;
|
|
|
|
}
|
|
|
|
if (x + rect.width > innerWidth) {
|
|
|
|
menuStyles.left -= rect.width;
|
|
|
|
}
|
|
|
|
if (menuStyles.top < 0) {
|
2024-08-26 17:35:36 +08:00
|
|
|
menuStyles.top = rect.bottom > innerHeight ? (innerHeight - 10 - rect.height) / 2 : 0;
|
2024-08-20 11:21:19 +08:00
|
|
|
}
|
|
|
|
if (menuStyles.left < 0) {
|
|
|
|
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
|
|
|
}
|
|
|
|
return menuStyles;
|
2024-08-26 17:35:36 +08:00
|
|
|
}, [getTableContentRect, getTableCanvasContainerRect]);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const handleShow = (event) => {
|
|
|
|
event.preventDefault();
|
|
|
|
if (menuRef.current && menuRef.current.contains(event.target)) return;
|
|
|
|
|
|
|
|
setVisible(true);
|
|
|
|
|
|
|
|
const position = getMenuPosition(event.clientX, event.clientY);
|
|
|
|
setPosition(position);
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener('contextmenu', handleShow);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('contextmenu', handleShow);
|
|
|
|
};
|
2024-09-29 17:52:25 +08:00
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2024-08-20 11:21:19 +08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (visible) {
|
|
|
|
document.addEventListener('mousedown', handleHide);
|
|
|
|
} else {
|
|
|
|
document.removeEventListener('mousedown', handleHide);
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('mousedown', handleHide);
|
|
|
|
};
|
|
|
|
}, [visible, handleHide]);
|
|
|
|
|
2024-12-02 16:42:22 +08:00
|
|
|
const renderMenu = useCallback(() => {
|
|
|
|
if (!visible) return null;
|
|
|
|
if (options.length === 0) return null;
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
ref={menuRef}
|
|
|
|
className='dropdown-menu sf-metadata-contextmenu'
|
|
|
|
style={position}
|
|
|
|
>
|
|
|
|
{options.map((option, index) => (
|
|
|
|
<button
|
|
|
|
key={index}
|
|
|
|
className='dropdown-item sf-metadata-contextmenu-item'
|
|
|
|
onClick={(event) => handleOptionClick(event, option)}
|
|
|
|
>
|
|
|
|
{option.label}
|
|
|
|
</button>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}, [visible, options, position, handleOptionClick]);
|
2024-08-20 11:21:19 +08:00
|
|
|
|
|
|
|
return (
|
2024-12-02 16:42:22 +08:00
|
|
|
<>
|
|
|
|
{renderMenu()}
|
2024-12-13 10:16:52 +08:00
|
|
|
{fileTagsRecord && (
|
|
|
|
<FileTagsDialog record={fileTagsRecord} onToggle={toggleFileTagsRecord} onSubmit={updateFileTags} />
|
2024-12-02 16:42:22 +08:00
|
|
|
)}
|
|
|
|
</>
|
|
|
|
|
2024-08-20 11:21:19 +08:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
ContextMenu.propTypes = {
|
|
|
|
isGroupView: PropTypes.bool,
|
|
|
|
selectedRange: PropTypes.object,
|
|
|
|
selectedPosition: PropTypes.object,
|
|
|
|
recordMetrics: PropTypes.object,
|
2024-09-29 17:52:25 +08:00
|
|
|
selectNone: PropTypes.func,
|
2024-08-26 17:35:36 +08:00
|
|
|
getTableContentRect: PropTypes.func,
|
2024-08-20 11:21:19 +08:00
|
|
|
recordGetterByIndex: PropTypes.func,
|
2024-09-29 17:52:25 +08:00
|
|
|
deleteRecords: PropTypes.func,
|
2024-08-20 11:21:19 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
export default ContextMenu;
|