diff --git a/frontend/src/components/toolbar/face-recognition-files-toolbar.js b/frontend/src/components/toolbar/face-recognition-files-toolbar.js
new file mode 100644
index 0000000000..8afb4200da
--- /dev/null
+++ b/frontend/src/components/toolbar/face-recognition-files-toolbar.js
@@ -0,0 +1,125 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
+import { gettext } from '../../utils/constants';
+import { EVENT_BUS_TYPE, GALLERY_OPERATION_KEYS } from '../../metadata/constants';
+import { useFileOperations } from '../../hooks/file-operations';
+
+const FaceRecognitionFilesToolbar = () => {
+ const [selectedRecordIds, setSelectedRecordIds] = useState([]);
+ const [selectedRecords, setSelectedRecords] = useState([]);
+ const [isSomeone, setIsSomeone] = useState(false);
+ const menuRef = useRef(null);
+ const { handleDownload: handleDownloadAPI } = useFileOperations();
+ const eventBus = window.sfMetadataContext && window.sfMetadataContext.eventBus;
+
+ const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
+ const canRemovePhotoFromPeople = window.sfMetadataContext.canRemovePhotoFromPeople();
+ const canAddPhotoToPeople = window.sfMetadataContext.canAddPhotoToPeople();
+ const canSetPeoplePhoto = window.sfMetadataContext.canSetPeoplePhoto();
+
+ useEffect(() => {
+ const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_RECORDS, (ids, metadata, selectedRecords, isSomeone) => {
+ setSelectedRecordIds(ids);
+ setSelectedRecords(selectedRecords);
+ setIsSomeone(isSomeone);
+ });
+
+ return () => {
+ unsubscribeSelectedFileIds && unsubscribeSelectedFileIds();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const unSelect = useCallback(() => {
+ setSelectedRecordIds([]);
+ eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []);
+ eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
+ }, [eventBus]);
+
+ const handleDownload = useCallback(() => {
+ const list = selectedRecords.map(record => {
+ const { parentDir, name: fileName } = record || {};
+ const name = parentDir === '/' ? fileName : `${parentDir}/${fileName}`;
+ return { name };
+ });
+ handleDownloadAPI('/', list);
+ }, [selectedRecords, handleDownloadAPI]);
+
+ const deleteRecords = useCallback(() => {
+ eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_FACE_RECOGNITION_RECORDS, selectedRecords, {
+ success_callback: () => {
+ eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
+ }
+ });
+ }, [eventBus, selectedRecords]);
+
+ const opList = useMemo(() => {
+ const list = [];
+ if (isSomeone && canRemovePhotoFromPeople) {
+ list.push({
+ key: GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET,
+ value: gettext('Remove from this group')
+ });
+ }
+ if (!isSomeone && canAddPhotoToPeople) {
+ list.push({
+ key: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS,
+ value: gettext('Add to groups')
+ });
+ }
+ if (canSetPeoplePhoto && selectedRecordIds.length == 1) {
+ list.push({
+ key: GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER,
+ value: gettext('Set as cover photo')
+ });
+ }
+ return list;
+ }, [isSomeone, selectedRecordIds, canRemovePhotoFromPeople, canAddPhotoToPeople, canSetPeoplePhoto]);
+
+ const getMenuList = useCallback(() => {
+ return opList;
+ }, [opList]);
+
+ const onMenuItemClick = useCallback((operation) => {
+ switch (operation) {
+ case GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET:
+ eventBus && eventBus.dispatch(EVENT_BUS_TYPE.REMOVE_PHOTOS_FROM_CURRENT_SET, selectedRecords);
+ break;
+ case GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS:
+ eventBus && eventBus.dispatch(EVENT_BUS_TYPE.ADD_PHOTO_TO_GROUPS);
+ break;
+ case GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER:
+ eventBus && eventBus.dispatch(EVENT_BUS_TYPE.SET_PHOTO_AS_COVER, selectedRecords[0]);
+ break;
+ default:
+ return;
+ }
+ }, [eventBus, selectedRecords]);
+
+ const length = selectedRecordIds.length;
+ return (
+
+
+
+ {length}{' '}{gettext('selected')}
+
+
+
+
+ {checkCanDeleteRow &&
+
+
+
+ }
+
+
+ );
+};
+
+export default FaceRecognitionFilesToolbar;
diff --git a/frontend/src/components/toolbar/metadata-path-toolbar.js b/frontend/src/components/toolbar/metadata-path-toolbar.js
index dcb42cb485..4af2c8406b 100644
--- a/frontend/src/components/toolbar/metadata-path-toolbar.js
+++ b/frontend/src/components/toolbar/metadata-path-toolbar.js
@@ -8,6 +8,7 @@ import AllTagsToolbar from './all-tags-toolbar';
import TagFilesToolbar from './tag-files-toolbar';
import TableFilesToolbar from './table-files-toolbar';
import GalleryFilesToolbar from './gallery-files-toolbar';
+import FaceRecognitionFilesToolbar from './face-recognition-files-toolbar';
const MetadataPathToolbar = ({ repoID, repoInfo, mode, path, viewId }) => {
const { idViewMap } = useMetadata();
@@ -20,6 +21,10 @@ const MetadataPathToolbar = ({ repoID, repoInfo, mode, path, viewId }) => {
);
}
+ if (type === VIEW_TYPE.FACE_RECOGNITION) {
+ return ;
+ }
+
if (type === VIEW_TYPE.TABLE) {
return (
diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js
index 66c46004c9..a3d85b5a4b 100644
--- a/frontend/src/metadata/constants/event-bus-type.js
+++ b/frontend/src/metadata/constants/event-bus-type.js
@@ -85,6 +85,12 @@ export const EVENT_BUS_TYPE = {
MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear',
SWITCH_GALLERY_GROUP_BY: 'switch_gallery_group_by',
+ // face recognition
+ DELETE_FACE_RECOGNITION_RECORDS: 'delete_face_recognition_records',
+ REMOVE_PHOTOS_FROM_CURRENT_SET: 'remove_photos_from_current_set',
+ SET_PHOTO_AS_COVER: 'set_photo_as_cover',
+ ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups',
+
// kanban
TOGGLE_KANBAN_SETTINGS: 'toggle_kanban_settings',
OPEN_KANBAN_SETTINGS: 'open_kanban_settings',
diff --git a/frontend/src/metadata/constants/view/gallery.js b/frontend/src/metadata/constants/view/gallery.js
index 966b0a6c5b..387262d43c 100644
--- a/frontend/src/metadata/constants/view/gallery.js
+++ b/frontend/src/metadata/constants/view/gallery.js
@@ -22,8 +22,8 @@ export const STORAGE_GALLERY_ZOOM_GEAR_KEY = 'gallery_zoom_gear';
export const GALLERY_OPERATION_KEYS = {
DOWNLOAD: 'download',
DELETE: 'delete',
- DUPLICATE: 'duplicate',
- REMOVE: 'remove',
- SET_PEOPLE_PHOTO: 'set_people_photo',
+ COPY: 'copy',
+ REMOVE_PHOTO_FROM_CURRENT_SET: 'remove_photo_from_current_set',
+ SET_PHOTO_AS_COVER: 'set_photo_as_cover',
ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups'
};
diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js
index 5d205a8c80..a1ef9c7a0d 100644
--- a/frontend/src/metadata/hooks/metadata-view.js
+++ b/frontend/src/metadata/hooks/metadata-view.js
@@ -296,10 +296,14 @@ export const MetadataViewProvider = ({
storeRef.current.updateFileTags(data);
}, [storeRef, modifyLocalFileTags]);
- const updateSelectedRecordIds = useCallback((ids) => {
+ const updateSelectedRecordIds = useCallback((ids, records, isSomeone) => {
toggleShowDirentToolbar(ids.length > 0);
setTimeout(() => {
- window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata);
+ if (records != undefined) {
+ window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata, records, isSomeone);
+ } else {
+ window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata);
+ }
}, 0);
}, [metadata, toggleShowDirentToolbar]);
diff --git a/frontend/src/metadata/views/face-recognition/person-photos/index.js b/frontend/src/metadata/views/face-recognition/person-photos/index.js
index 24a9077451..ac875d8260 100644
--- a/frontend/src/metadata/views/face-recognition/person-photos/index.js
+++ b/frontend/src/metadata/views/face-recognition/person-photos/index.js
@@ -237,12 +237,14 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople
const eventBus = window?.sfMetadataContext?.eventBus;
if (!eventBus) return;
const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange);
+ const unsubscribeDeleteRecords = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_FACE_RECOGNITION_RECORDS, handleDelete);
const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange);
return () => {
unsubscribeViewChange && unsubscribeViewChange();
+ unsubscribeDeleteRecords && unsubscribeDeleteRecords();
localRecordChangedSubscribe && localRecordChangedSubscribe();
};
- }, [onViewChange, onRecordChange]);
+ }, [onViewChange, handleDelete, onRecordChange]);
if (isLoading) return ();
@@ -256,6 +258,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople
onRemoveImage={people._is_someone ? handleRemove : null}
onAddImage={!people._is_someone ? handleAdd : null}
onSetPeoplePhoto={handleSetPeoplePhoto}
+ isSomeone={people._is_someone}
/>
);
diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js
index 861baba96c..c3586fe5c9 100644
--- a/frontend/src/metadata/views/gallery/context-menu/index.js
+++ b/frontend/src/metadata/views/gallery/context-menu/index.js
@@ -1,16 +1,12 @@
-import React, { useMemo, useCallback, useState } from 'react';
+import React, { useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import ContextMenu from '../../../components/context-menu';
-import ModalPortal from '../../../../components/modal-portal';
-import PeoplesDialog from '../../../components/dialog/peoples-dialog';
import { gettext } from '../../../../utils/constants';
import { Dirent } from '../../../../models';
import { useFileOperations } from '../../../../hooks/file-operations';
import { GALLERY_OPERATION_KEYS } from '../../../constants';
const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveImage, onAddImage, onSetPeoplePhoto }) => {
- const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false);
-
const { handleDownload: handleDownloadAPI, handleCopy: handleCopyAPI } = useFileOperations();
const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
@@ -22,22 +18,39 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma
const options = useMemo(() => {
let validOptions = [{ value: GALLERY_OPERATION_KEYS.DOWNLOAD, label: gettext('Download') }];
if (onDelete && checkCanDeleteRow) {
- validOptions.push({ value: GALLERY_OPERATION_KEYS.DELETE, label: gettext('Delete') });
+ validOptions.push({
+ value: GALLERY_OPERATION_KEYS.DELETE,
+ label: gettext('Delete')
+ });
}
if (onDuplicate && canDuplicateRow && selectedImages.length === 1) {
- validOptions.push({ value: GALLERY_OPERATION_KEYS.DUPLICATE, label: gettext('Copy') });
+ validOptions.push({
+ value: GALLERY_OPERATION_KEYS.COPY,
+ label: gettext('Copy')
+ });
}
if (onRemoveImage && canRemovePhotoFromPeople) {
- validOptions.push({ value: GALLERY_OPERATION_KEYS.REMOVE, label: gettext('Remove from this group') });
+ validOptions.push({
+ value: GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET,
+ label: gettext('Remove from this group')
+ });
}
if (onAddImage && canAddPhotoToPeople) {
- validOptions.push({ value: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS, label: gettext('Add to groups') });
+ validOptions.push({
+ value: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS,
+ label: gettext('Add to groups')
+ });
}
- if (onSetPeoplePhoto && canSetPeoplePhoto) {
- validOptions.push({ value: GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO, label: gettext('Set as cover photo') });
+ if (onSetPeoplePhoto && canSetPeoplePhoto && selectedImages.length === 1) {
+ validOptions.push({
+ value: GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER,
+ label: gettext('Set as cover photo')
+ });
}
return validOptions;
- }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]);
+ }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople,
+ canAddPhotoToPeople, selectedImages, onDuplicate, onDelete,
+ onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]);
const handleDuplicate = useCallback((destRepo, dirent, destPath, nodeParentPath, isByDialog) => {
const selectedImage = selectedImages[0];
@@ -68,44 +81,29 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma
case GALLERY_OPERATION_KEYS.DELETE:
onDelete(selectedImages);
break;
- case GALLERY_OPERATION_KEYS.DUPLICATE:
+ case GALLERY_OPERATION_KEYS.COPY:
handleCopy();
break;
- case GALLERY_OPERATION_KEYS.REMOVE:
+ case GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET:
onRemoveImage(selectedImages);
break;
case GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS:
- setPeoplesDialogShow(true);
+ onAddImage();
break;
- case GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO:
+ case GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER:
onSetPeoplePhoto(selectedImages[0]);
break;
default:
break;
}
- }, [handleDownload, onDelete, selectedImages, handleCopy, onRemoveImage, onSetPeoplePhoto]);
-
- const closePeoplesDialog = useCallback(() => {
- setPeoplesDialogShow(false);
- }, []);
-
- const addPeople = useCallback((peopleIds, addedImages, callback) => {
- onAddImage(peopleIds, addedImages, callback);
- }, [onAddImage]);
+ }, [handleDownload, onDelete, selectedImages, handleCopy, onRemoveImage, onAddImage, onSetPeoplePhoto]);
return (
- <>
-
- {isPeoplesDialogShow && (
-
-
-
- )}
- >
+
);
};
diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js
index 9423db1c11..2ad16cdb5a 100644
--- a/frontend/src/metadata/views/gallery/main.js
+++ b/frontend/src/metadata/views/gallery/main.js
@@ -15,12 +15,13 @@ import { getEventClassName } from '../../../utils/dom';
import { getColumns, getImageSize, getRowHeight } from './utils';
import ObjectUtils from '../../../utils/object';
import { openFile } from '../../utils/file';
+import PeoplesDialog from '../../components/dialog/peoples-dialog';
import './index.css';
const OVER_SCAN_ROWS = 20;
-const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onRemoveImage, onAddImage, onSetPeoplePhoto }) => {
+const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onRemoveImage, onAddImage, onSetPeoplePhoto, isSomeone }) => {
const [isFirstLoading, setFirstLoading] = useState(true);
const [zoomGear, setZoomGear] = useState(0);
const [containerWidth, setContainerWidth] = useState(0);
@@ -30,6 +31,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
const [imageIndex, setImageIndex] = useState(0);
const [selectedImages, setSelectedImages] = useState([]);
const [lastSelectedImage, setLastSelectedImage] = useState(null);
+ const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false);
const containerRef = useRef(null);
const scrollContainer = useRef(null);
@@ -193,8 +195,12 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
const updateSelectedImages = useCallback((selectedImages) => {
const ids = selectedImages.map(item => item.id);
- updateSelectedRecordIds(ids);
- }, [updateSelectedRecordIds]);
+ if (isSomeone != undefined) { // 'face recognition'
+ updateSelectedRecordIds(ids, selectedImages, isSomeone);
+ } else {
+ updateSelectedRecordIds(ids);
+ }
+ }, [isSomeone, updateSelectedRecordIds]);
const handleClick = useCallback((event, image) => {
if (event.metaKey || event.ctrlKey) {
@@ -301,7 +307,22 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
});
}, [onRemoveImage, updateCurrentDirent, updateSelectedImages]);
- const handleMakeSelectedAsCoverPhoto = useCallback((selectedImage) => {
+ const handleAddPhotoToGroup = useCallback((selectedImages) => {
+ setPeoplesDialogShow(true);
+ }, []);
+
+ const closePeoplesDialog = useCallback(() => {
+ setPeoplesDialogShow(false);
+ }, []);
+
+ const addPhotoToGroup = useCallback((peopleIds, addedImages, callback) => {
+ onAddImage(peopleIds, addedImages, callback);
+ updateCurrentDirent();
+ setSelectedImages([]);
+ updateSelectedImages([]);
+ }, [onAddImage, updateCurrentDirent, updateSelectedImages]);
+
+ const setSelectedImageAsCover = useCallback((selectedImage) => {
onSetPeoplePhoto(selectedImage, {
success_callback: () => {
updateCurrentDirent();
@@ -409,11 +430,17 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
});
const unsubscribeSelectNone = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, selectNone);
+ const unsubscribeRemovePhotosFromCurrentSet = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.REMOVE_PHOTOS_FROM_CURRENT_SET, handleRemoveSelectedImages);
+ const unsubscribeSetPhotoAsCover = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SET_PHOTO_AS_COVER, setSelectedImageAsCover);
+ const unsubscribeAddPhotoToGroups = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.ADD_PHOTO_TO_GROUPS, handleAddPhotoToGroup);
return () => {
container && resizeObserver.unobserve(container);
modifyGalleryZoomGearSubscribe();
unsubscribeSelectNone();
+ unsubscribeRemovePhotosFromCurrentSet();
+ unsubscribeSetPhotoAsCover();
+ unsubscribeAddPhotoToGroups();
switchGalleryModeSubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -481,8 +508,8 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
onDelete={handleDeleteSelectedImages}
onDuplicate={duplicateRecord}
onRemoveImage={onRemoveImage ? handleRemoveSelectedImages : null}
- onAddImage={onAddImage}
- onSetPeoplePhoto={handleMakeSelectedAsCoverPhoto}
+ onAddImage={onAddImage ? handleAddPhotoToGroup : null}
+ onSetPeoplePhoto={setSelectedImageAsCover}
/>
{isImagePopupOpen && (
@@ -498,6 +525,11 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord,
/>
)}
+ {isPeoplesDialogShow && (
+
+
+
+ )}
);
};
diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js
index 823cfd0630..7a5b580b3c 100644
--- a/frontend/src/pages/lib-content-view/lib-content-view.js
+++ b/frontend/src/pages/lib-content-view/lib-content-view.js
@@ -2473,7 +2473,13 @@ class LibContentView extends React.Component {
})}>
{isDirentSelected ? (
currentMode === TAGS_MODE || currentMode === METADATA_MODE ? (
-
+
) : (