From 206e9a3c1b3f6726af1f7d20ddff6f7c53c095c1 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 21 Oct 2024 16:48:40 -0700 Subject: [PATCH 01/18] Preliminary frontend implementation --- src/components/HydratedModal.jsx | 7 ++ src/features/filters/FiltersPanelFooter.jsx | 50 ++++++++++++-- src/features/images/DeleteImagesModal.jsx | 73 ++++++++++++++++++++ src/features/tasks/tasksSlice.js | 76 +++++++++++++++++++++ 4 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 src/features/images/DeleteImagesModal.jsx diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index b2389b09..3486124c 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import { Modal } from './Modal.jsx'; import ImagesStatsModal from '../features/images/ImagesStatsModal.jsx'; import ExportModal from '../features/images/ExportModal.jsx'; +import DeleteImagesModal from '../features/images/DeleteImagesModal.jsx'; import CameraAdminModal from '../features/cameras/CameraAdminModal.jsx'; import AutomationRulesForm from '../features/projects/AutomationRulesForm.jsx'; import SaveViewForm from '../features/projects/SaveViewForm.jsx'; @@ -65,6 +66,12 @@ const HydratedModal = () => { content: , callBackOnClose: () => dispatch(clearExport()), }, + 'delete-images': { + title: 'Delete Selected Images', + size: 'md', + content: , + callBackOnClose: () => dispatch(clearExport()), + }, 'camera-admin-modal': { title: 'Manage Cameras and Deployments', size: 'md', diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index d9acdff9..9f877edd 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { styled, keyframes } from '../../theme/stitches.config.js'; import { selectUserCurrentRoles } from '../auth/authSlice.js'; -import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES } from '../auth/roles.js'; +import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES, WRITE_IMAGES_ROLES } from '../auth/roles.js'; import { useDispatch, useSelector } from 'react-redux'; import { selectImagesCount, selectImagesCountLoading } from '../images/imagesSlice.js'; import { @@ -12,9 +12,14 @@ import { fetchProjects, } from '../projects/projectsSlice.js'; import { toggleOpenLoupe } from '../loupe/loupeSlice.js'; -import { InfoCircledIcon, DownloadIcon, SymbolIcon } from '@radix-ui/react-icons'; +import { InfoCircledIcon, DownloadIcon, SymbolIcon, TrashIcon } from '@radix-ui/react-icons'; import IconButton from '../../components/IconButton.jsx'; -import { Tooltip, TooltipContent, TooltipArrow, TooltipTrigger } from '../../components/Tooltip.jsx'; +import { + Tooltip, + TooltipContent, + TooltipArrow, + TooltipTrigger, +} from '../../components/Tooltip.jsx'; const RefreshButton = styled('div', { height: '100%', @@ -40,6 +45,14 @@ const ExportCSVButton = styled('div', { padding: '0 $1', }); +const DeleteImagesButton = styled('div', { + height: '100%', + borderLeft: '1px solid $border', + display: 'flex', + alignItems: 'center', + padding: '0 $1', +}); + const ImagesCount = styled('div', { flexGrow: 1, display: 'flex', @@ -115,7 +128,11 @@ const FiltersPanelFooter = () => { - handleModalToggle('stats-modal')}> + handleModalToggle('stats-modal')} + > @@ -130,7 +147,11 @@ const FiltersPanelFooter = () => { - handleModalToggle('export-modal')}> + handleModalToggle('export-modal')} + > @@ -141,6 +162,25 @@ const FiltersPanelFooter = () => { )} + {hasRole(userRoles, WRITE_IMAGES_ROLES) && ( + + + + handleModalToggle('delete-images')} + > + + + + + + Delete all images shown + + + + )} diff --git a/src/features/images/DeleteImagesModal.jsx b/src/features/images/DeleteImagesModal.jsx new file mode 100644 index 00000000..1bdecb84 --- /dev/null +++ b/src/features/images/DeleteImagesModal.jsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; +import { selectActiveFilters } from '../filters/filtersSlice.js'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner'; +import { ButtonRow, HelperText } from '../../components/Form'; +import Button from '../../components/Button'; +import NoneFoundAlert from '../../components/NoneFoundAlert'; +import { fetchImagesCount, fetchImages, deleteImages } from './imagesSlice.js'; + +const DeleteImagesModal = async () => { + const filters = useSelector(selectActiveFilters); + const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const dispatch = useDispatch(); + + const imageCount = await dispatch(fetchImagesCount(filters)); + + const deleteImagesPending = deleteImagesLoading.isLoading && deleteImagesLoading.taskId; + useEffect(() => { + if (deleteImagesPending) { + dispatch(fetchTask(deleteImagesLoading.taskId)); + } + }, [deleteImagesPending, deleteImagesLoading, dispatch]); + + const handleDeleteImagesButtonClick = () => { + const { isLoading, errors, noneFound } = deleteImagesLoading; + const noErrors = !errors || errors.length === 0; + if (!noneFound && !isLoading && noErrors) { + // const images = dispatch(fetchImages(filters)); + if (imageCount > 50) { + dispatch(deleteImagesTask({ filters })); + } else { + dispatch(fetchImages(filters)).then((images) => { + dispatch(deleteImages({ images })); + }); + } + } + }; + + return ( +
+ {deleteImagesLoading.isLoading && ( + + + + )} + {imageCount === 0 && ( + + We couldn't find any images that matched this set of filters. + + )} + +

+ Do you wish to delete all images that match the current filters? This action will delete + {imageCount} images and cannot be undone. +

+
+ + + +
+ ); +}; + +export default DeleteImagesModal; diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 211cca19..19857143 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -42,6 +42,11 @@ const initialState = { isLoading: false, errors: null, }, + deleteImagesTask: { + taskId: null, + isLoading: false, + errors: null + } }, imagesStats: null, annotationsExport: null, @@ -254,6 +259,42 @@ export const tasksSlice = createSlice({ state.loadingStates.cameraSerialNumber.taskId = null; state.loadingStates.cameraSerialNumber.errors.splice(index, 1); }, + + // delete images + + deleteImagesTaskStart: (state) => { + let ls = state.loadingStates.deleteImages; + ls.taskId = null; + ls.isLoading = true; + ls.errors = null; + }, + + deleteImagesTaskUpdate: (state, { payload }) => { + state.loadingStates.deleteImages.taskId = payload.taskId; + }, + + deleteImagesTaskSuccess: (state) => { + let ls = state.loadingStates.deleteImages; + ls.taskId = null; + ls.isLoading = false; + ls.errors = null; + }, + + deleteImagesTaskFailure: (state, { payload }) => { + let ls = state.loadingStates.deleteImages; + ls.isLoading = false; + ls.errors = [payload.task.output.error]; + }, + + clearDeleteImagesTask: (state) => { + state.loadingStates.deleteImages = initialState.loadingStates.deleteImages; + }, + + dismissDeleteImagesTaskError: (state, { payload }) => { + const index = payload; + state.loadingStates.deleteImages.taskId = null; + state.loadingStates.deleteImages.errors.splice(index, 1); + }, }, }); @@ -294,6 +335,13 @@ export const { updateCameraSerialNumberFailure, clearCameraSerialNumberTask, dismissCameraSerialNumberError, + + deleteImagesTaskStart, + deleteImagesTaskUpdate, + deleteImagesTaskSuccess, + deleteImagesTaskFailure, + clearDeleteImagesTask, + dismissDeleteImagesTaskError, } = tasksSlice.actions; // fetchTask thunk @@ -505,6 +553,33 @@ export const updateCameraSerialNumber = (payload) => { }; }; +// delete images thunk +export const deleteImagesTask = ({ filters }) => { + return async (dispatch, getState) => { + try { + dispatch(updateCameraSerialNumberStart()); + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + const projects = getState().projects.projects; + const selectedProj = projects.find((proj) => proj.selected); + console.log(filters); + + if (token && selectedProj) { + const res = await call({ + projId: selectedProj._id, + request: 'deleteImagesTask', + input: filters, + }); + console.log('deleteImages - res: ', res); + dispatch(deleteImagesTaskUpdate({ taskId: res.deleteImagesTask._id })); + } + } + catch (err) { + dispatch(updateCameraSerialNumberFailure(err)); + } + }; +}; + export const selectImagesStats = (state) => state.tasks.imagesStats; export const selectStatsLoading = (state) => state.tasks.loadingStates.stats; export const selectStatsErrors = (state) => state.tasks.loadingStates.stats.errors; @@ -522,5 +597,6 @@ export const selectCameraSerialNumberLoading = (state) => state.tasks.loadingStates.cameraSerialNumber; export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; +export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImagesTask; export default tasksSlice.reducer; From 54cb4de34af92983e59d33651582af4507514b5a Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 23 Oct 2024 17:30:42 -0700 Subject: [PATCH 02/18] creating new delete by filter --- src/components/HydratedModal.jsx | 14 ++- src/features/images/DeleteImagesModal.jsx | 30 +++--- src/features/tasks/tasksSlice.js | 120 ++++++++++++++++++---- 3 files changed, 131 insertions(+), 33 deletions(-) diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index 3486124c..3f4a70b7 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -24,6 +24,8 @@ import { selectDeploymentsLoading, selectCameraSerialNumberLoading, clearCameraSerialNumberTask, + selectDeleteImagesLoading, + selectDeleteImagesByFilterLoading, } from '../features/tasks/tasksSlice.js'; import { selectModalOpen, @@ -46,12 +48,16 @@ const HydratedModal = () => { const errorsExportLoading = useSelector(selectErrorsExportLoading); const deploymentsLoading = useSelector(selectDeploymentsLoading); const cameraSerialNumberLoading = useSelector(selectCameraSerialNumberLoading); + const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const deleteImagesByFilterLoading = useSelector(selectDeleteImagesByFilterLoading); const asyncTaskLoading = statsLoading.isLoading || annotationsExportLoading.isLoading || errorsExportLoading.isLoading || deploymentsLoading.isLoading || - cameraSerialNumberLoading.isLoading; + cameraSerialNumberLoading.isLoading || + deleteImagesLoading.isLoading || + deleteImagesByFilterLoading.isLoading; const modalContentMap = { 'stats-modal': { @@ -128,6 +134,12 @@ const HydratedModal = () => { dispatch(clearCameraSerialNumberTask()); }, }, + 'delete-images-by-filter': { + title: 'Delete Filtered Images', + size: 'md', + content: , + callBackOnClose: () => true, + }, }; const handleModalToggle = (content) => { diff --git a/src/features/images/DeleteImagesModal.jsx b/src/features/images/DeleteImagesModal.jsx index 1bdecb84..040fdb62 100644 --- a/src/features/images/DeleteImagesModal.jsx +++ b/src/features/images/DeleteImagesModal.jsx @@ -1,6 +1,10 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; +import { + deleteImagesByFilterTask, + fetchTask, + selectDeleteImagesByFilterLoading, +} from '../tasks/tasksSlice.js'; import { selectActiveFilters } from '../filters/filtersSlice.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner'; import { ButtonRow, HelperText } from '../../components/Form'; @@ -10,25 +14,25 @@ import { fetchImagesCount, fetchImages, deleteImages } from './imagesSlice.js'; const DeleteImagesModal = async () => { const filters = useSelector(selectActiveFilters); - const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const deleteImagesByFilterLoading = useSelector(selectDeleteImagesByFilterLoading); const dispatch = useDispatch(); const imageCount = await dispatch(fetchImagesCount(filters)); - const deleteImagesPending = deleteImagesLoading.isLoading && deleteImagesLoading.taskId; + const deleteImagesPending = + deleteImagesByFilterLoading.isLoading && deleteImagesByFilterLoading.taskId; useEffect(() => { if (deleteImagesPending) { - dispatch(fetchTask(deleteImagesLoading.taskId)); + dispatch(fetchTask(deleteImagesByFilterLoading.taskId)); } - }, [deleteImagesPending, deleteImagesLoading, dispatch]); + }, [deleteImagesPending, deleteImagesByFilterLoading, dispatch]); - const handleDeleteImagesButtonClick = () => { - const { isLoading, errors, noneFound } = deleteImagesLoading; + const handleDeleteImagesButtonClick = async () => { + const { isLoading, errors } = deleteImagesByFilterLoading; const noErrors = !errors || errors.length === 0; - if (!noneFound && !isLoading && noErrors) { - // const images = dispatch(fetchImages(filters)); - if (imageCount > 50) { - dispatch(deleteImagesTask({ filters })); + if (!isLoading && noErrors) { + if (imageCount > 100) { + dispatch(deleteImagesByFilterTask({ filters })); } else { dispatch(fetchImages(filters)).then((images) => { dispatch(deleteImages({ images })); @@ -39,7 +43,7 @@ const DeleteImagesModal = async () => { return (
- {deleteImagesLoading.isLoading && ( + {deleteImagesByFilterLoading.isLoading && ( @@ -59,7 +63,7 @@ const DeleteImagesModal = async () => { + + + + )} +
+ ); +}; + +export default DeleteImagesByFilterModal; diff --git a/src/features/images/DeleteImagesModal.jsx b/src/features/images/DeleteImagesModal.jsx deleted file mode 100644 index 040fdb62..00000000 --- a/src/features/images/DeleteImagesModal.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { - deleteImagesByFilterTask, - fetchTask, - selectDeleteImagesByFilterLoading, -} from '../tasks/tasksSlice.js'; -import { selectActiveFilters } from '../filters/filtersSlice.js'; -import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner'; -import { ButtonRow, HelperText } from '../../components/Form'; -import Button from '../../components/Button'; -import NoneFoundAlert from '../../components/NoneFoundAlert'; -import { fetchImagesCount, fetchImages, deleteImages } from './imagesSlice.js'; - -const DeleteImagesModal = async () => { - const filters = useSelector(selectActiveFilters); - const deleteImagesByFilterLoading = useSelector(selectDeleteImagesByFilterLoading); - const dispatch = useDispatch(); - - const imageCount = await dispatch(fetchImagesCount(filters)); - - const deleteImagesPending = - deleteImagesByFilterLoading.isLoading && deleteImagesByFilterLoading.taskId; - useEffect(() => { - if (deleteImagesPending) { - dispatch(fetchTask(deleteImagesByFilterLoading.taskId)); - } - }, [deleteImagesPending, deleteImagesByFilterLoading, dispatch]); - - const handleDeleteImagesButtonClick = async () => { - const { isLoading, errors } = deleteImagesByFilterLoading; - const noErrors = !errors || errors.length === 0; - if (!isLoading && noErrors) { - if (imageCount > 100) { - dispatch(deleteImagesByFilterTask({ filters })); - } else { - dispatch(fetchImages(filters)).then((images) => { - dispatch(deleteImages({ images })); - }); - } - } - }; - - return ( -
- {deleteImagesByFilterLoading.isLoading && ( - - - - )} - {imageCount === 0 && ( - - We couldn't find any images that matched this set of filters. - - )} - -

- Do you wish to delete all images that match the current filters? This action will delete - {imageCount} images and cannot be undone. -

-
- - - -
- ); -}; - -export default DeleteImagesModal; diff --git a/src/features/loupe/DeleteImagesAlert.jsx b/src/features/loupe/DeleteImagesAlert.jsx index 73202a2e..6eed0bb7 100644 --- a/src/features/loupe/DeleteImagesAlert.jsx +++ b/src/features/loupe/DeleteImagesAlert.jsx @@ -1,10 +1,22 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { deleteImages, setDeleteImagesAlertOpen, selectDeleteImagesAlertOpen } from '../images/imagesSlice.js'; +import { + deleteImages, + setDeleteImagesAlertOpen, + selectDeleteImagesAlertOpen, +} from '../images/imagesSlice.js'; import { selectSelectedImages } from '../review/reviewSlice.js'; -import { Alert, AlertPortal, AlertOverlay, AlertContent, AlertTitle } from '../../components/AlertDialog.jsx'; +import { + Alert, + AlertPortal, + AlertOverlay, + AlertContent, + AlertTitle, +} from '../../components/AlertDialog.jsx'; import Button from '../../components/Button.jsx'; import { red } from '@radix-ui/colors'; +import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; +import { IMAGE_QUERY_LIMITS } from '../../config.js'; const DeleteImagesAlert = () => { const dispatch = useDispatch(); @@ -12,8 +24,20 @@ const DeleteImagesAlert = () => { const selectedImages = useSelector(selectSelectedImages); const selectedImageIds = selectedImages.map((img) => img._id); + const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + + useEffect(() => { + if (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) { + dispatch(fetchTask(deleteImagesLoading.taskId)); + } + }, [deleteImagesLoading, dispatch]); + const handleConfirmDelete = () => { - dispatch(deleteImages(selectedImageIds)); + if (selectedImages.length <= IMAGE_QUERY_LIMITS[2]) { + dispatch(deleteImages(selectedImageIds)); + } else { + dispatch(deleteImagesTask({ imageIds: selectedImageIds })); + } }; const handleCancelDelete = () => { diff --git a/src/features/review/reviewSlice.js b/src/features/review/reviewSlice.js index 5b95ca62..61a3538e 100644 --- a/src/features/review/reviewSlice.js +++ b/src/features/review/reviewSlice.js @@ -4,6 +4,7 @@ import { call } from '../../api'; import { findImage, findObject, findLabel, isImageReviewed } from '../../app/utils'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { getImagesSuccess, clearImages, deleteImagesSuccess } from '../images/imagesSlice'; +import { deleteImagesSuccess as deleteImageTaskSuccess } from '../tasks/tasksSlice'; const initialState = { workingImages: [], @@ -207,6 +208,9 @@ export const reviewSlice = createSlice({ }) .addCase(deleteImagesSuccess, (state, { payload }) => { state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); + }) + .addCase(deleteImageTaskSuccess, (state, { payload }) => { + state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); }); }, }); diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 95368ffc..47a8b7f8 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -9,6 +9,8 @@ import { setSelectedCamera, } from '../projects/projectsSlice'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; +import { setFocus, setSelectedImageIndices } from '../review/reviewSlice.js'; +import { fetchImages, setDeleteImagesAlertOpen } from '../images/imagesSlice.js'; const initialState = { loadingStates: { @@ -401,7 +403,7 @@ export const fetchTask = (taskId) => { const projects = getState().projects.projects; const selectedProj = projects.find((proj) => proj.selected); - if (token && selectedProj) { + if (token && selectedProj && taskId) { const res = await call({ projId: selectedProj._id, request: 'getTask', @@ -456,10 +458,25 @@ export const fetchTask = (taskId) => { FAIL: (res) => dispatch(updateCameraSerialNumberFailure(res)), }, DeleteImages: { - COMPLETE: () => { + COMPLETE: (res) => { + dispatch(deleteImagesSuccess(res.task.output.imageIds)); + }, + FAIL: (res) => dispatch(deleteImagesFailure(res)) + }, + DeleteImagesByFilter: { + COMPLETE: (res) => { dispatch(deleteImagesByFilterSuccess()); + dispatch( + setFocus({ + index: { image: null, object: null, label: null }, + type: 'auto', + }), + ); + dispatch(setModalOpen(false)); + dispatch(setModalContent(null)); + dispatch(fetchImages(res.task.output.filters)); }, - FAIL: (res) => dispatch(deleteImagesByFilterFailure(res)), + FAIL: (res) => dispatch(deleteImagesByFilterFailure(res)) } }; @@ -616,16 +633,23 @@ export const deleteImagesTask = ({ imageIds }) => { const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); const projects = getState().projects.projects; const selectedProj = projects.find((proj) => proj.selected); - console.log(imageIds) if (token && selectedProj) { const res = await call({ projId: selectedProj._id, request: 'deleteImagesTask', - input: imageIds, + input: { imageIds }, }); console.log('deleteImages - res: ', res); dispatch(deleteImagesUpdate({ taskId: res.deleteImagesTask._id })); + dispatch( + setFocus({ + index: { image: null, object: null, label: null }, + type: 'auto', + }), + ); + dispatch(setSelectedImageIndices([])); + dispatch(setDeleteImagesAlertOpen(false)); } } catch (err) { @@ -649,10 +673,10 @@ export const deleteImagesByFilterTask = ({ filters }) => { const res = await call({ projId: selectedProj._id, request: 'deleteImagesByFilterTask', - input: filters, + input: { filters }, }); console.log('deleteImagesByFilter - res: ', res); - dispatch(deleteImagesByFilterUpdate({ taskId: res.deleteImagesTask._id })); + dispatch(deleteImagesByFilterUpdate({ taskId: res.deleteImagesByFilterTask._id })); } } catch (err) { @@ -679,6 +703,6 @@ export const selectCameraSerialNumberLoading = (state) => export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImages; -export const selectDeleteImagesByFilterLoading = (state) => state.tasks.loadingStates.deleteImagesbyFilter; +export const selectDeleteImagesByFilterLoading = (state) => state.tasks.loadingStates.deleteImagesByFilter; export default tasksSlice.reducer; From dca2fb5577f65aaf7aaa0dc7f118c67aee807e3e Mon Sep 17 00:00:00 2001 From: jue-henry Date: Fri, 1 Nov 2024 10:05:19 -0700 Subject: [PATCH 04/18] fixing closing modal --- src/features/images/DeleteImagesByFilterModal.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/images/DeleteImagesByFilterModal.jsx b/src/features/images/DeleteImagesByFilterModal.jsx index a290aa85..d6c59eab 100644 --- a/src/features/images/DeleteImagesByFilterModal.jsx +++ b/src/features/images/DeleteImagesByFilterModal.jsx @@ -56,8 +56,8 @@ const DeleteImagesByFilterModal = () => { }, [deleteImagesByFilterLoading, imageCountIsLoading.isLoading]); const handleCancelDelete = () => { - setModalOpen(false); - setModalContent(null); + dispatch(setModalOpen(false)); + dispatch(setModalContent(null)); }; return ( From 2fa4d94f7cf817b79cf1998cd629585dae117e58 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 4 Nov 2024 14:21:10 -0800 Subject: [PATCH 05/18] refactoring to use just one alert --- src/components/HydratedModal.jsx | 15 +-- src/config.js | 2 + src/features/filters/FiltersPanelFooter.jsx | 10 +- .../images/DeleteImagesByFilterModal.jsx | 101 ----------------- src/features/images/ImagesTableRow.jsx | 46 ++++++-- src/features/images/imagesSlice.js | 16 ++- src/features/loupe/DeleteImagesAlert.jsx | 57 +++++++--- src/features/loupe/LoupeDropdown.jsx | 2 +- src/features/tasks/tasksSlice.js | 104 ++++-------------- 9 files changed, 125 insertions(+), 228 deletions(-) delete mode 100644 src/features/images/DeleteImagesByFilterModal.jsx diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index 2c51d03d..13efd114 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -4,7 +4,6 @@ import moment from 'moment-timezone'; import { Modal } from './Modal.jsx'; import ImagesStatsModal from '../features/images/ImagesStatsModal.jsx'; import ExportModal from '../features/images/ExportModal.jsx'; -import DeleteImagesByFilterModal from '../features/images/DeleteImagesByFilterModal.jsx'; import CameraAdminModal from '../features/cameras/CameraAdminModal.jsx'; import AutomationRulesForm from '../features/projects/AutomationRulesForm.jsx'; import SaveViewForm from '../features/projects/SaveViewForm.jsx'; @@ -25,8 +24,6 @@ import { selectCameraSerialNumberLoading, clearCameraSerialNumberTask, selectDeleteImagesLoading, - selectDeleteImagesByFilterLoading, - clearDeleteImagesByFilterTask, } from '../features/tasks/tasksSlice.js'; import { selectModalOpen, @@ -50,15 +47,13 @@ const HydratedModal = () => { const deploymentsLoading = useSelector(selectDeploymentsLoading); const cameraSerialNumberLoading = useSelector(selectCameraSerialNumberLoading); const deleteImagesLoading = useSelector(selectDeleteImagesLoading); - const deleteImagesByFilterLoading = useSelector(selectDeleteImagesByFilterLoading); const asyncTaskLoading = statsLoading.isLoading || annotationsExportLoading.isLoading || errorsExportLoading.isLoading || deploymentsLoading.isLoading || cameraSerialNumberLoading.isLoading || - deleteImagesLoading.isLoading || - deleteImagesByFilterLoading.isLoading; + deleteImagesLoading.isLoading; const modalContentMap = { 'stats-modal': { @@ -129,14 +124,6 @@ const HydratedModal = () => { dispatch(clearCameraSerialNumberTask()); }, }, - 'delete-images-by-filter': { - title: 'Delete Filtered Images', - size: 'md', - content: , - callBackOnClose: () => { - dispatch(clearDeleteImagesByFilterTask()); - }, - }, }; const handleModalToggle = (content) => { diff --git a/src/config.js b/src/config.js index 2b1f8982..700db5a8 100644 --- a/src/config.js +++ b/src/config.js @@ -16,6 +16,8 @@ const stage = import.meta.env.VITE_STAGE || process.env.NODE_ENV; export const API_URL = API_URLS[stage]; export const IMAGES_URL = IMAGES_URLS[stage]; export const IMAGE_QUERY_LIMITS = [10, 50, 100]; +export const IMAGE_DELETE_LIMIT = 100; + export const SUPPORTED_WIRELESS_CAMS = ['BuckEyeCam', 'RidgeTec', 'CUDDEBACK', 'RECONYX']; diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index 169e5b20..16f5b2a5 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -3,7 +3,11 @@ import { styled, keyframes } from '../../theme/stitches.config.js'; import { selectUserCurrentRoles } from '../auth/authSlice.js'; import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES, WRITE_IMAGES_ROLES } from '../auth/roles.js'; import { useDispatch, useSelector } from 'react-redux'; -import { selectImagesCount, selectImagesCountLoading } from '../images/imagesSlice.js'; +import { + selectImagesCount, + selectImagesCountLoading, + setDeleteImagesAlertOpen, +} from '../images/imagesSlice.js'; import { selectModalOpen, selectSelectedProject, @@ -169,7 +173,9 @@ const FiltersPanelFooter = () => { handleModalToggle('delete-images-by-filter')} + onClick={() => { + dispatch(setDeleteImagesAlertOpen(true)); + }} > diff --git a/src/features/images/DeleteImagesByFilterModal.jsx b/src/features/images/DeleteImagesByFilterModal.jsx deleted file mode 100644 index d6c59eab..00000000 --- a/src/features/images/DeleteImagesByFilterModal.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { - deleteImagesByFilterTask, - fetchTask, - selectDeleteImagesByFilterLoading, -} from '../tasks/tasksSlice.js'; -import { selectActiveFilters } from '../filters/filtersSlice.js'; -import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; -import { ButtonRow, HelperText } from '../../components/Form.jsx'; -import Button from '../../components/Button.jsx'; -import NoneFoundAlert from '../../components/NoneFoundAlert.jsx'; -import { deleteImages, selectImagesCount, selectImagesCountLoading } from './imagesSlice.js'; -import { setModalOpen, setModalContent } from '../projects/projectsSlice.js'; -import { IMAGE_QUERY_LIMITS } from '../../config'; -import { selectWorkingImages } from '../review/reviewSlice.js'; - -const DeleteImagesByFilterModal = () => { - const filters = useSelector(selectActiveFilters); - const deleteImagesByFilterLoading = useSelector(selectDeleteImagesByFilterLoading); - const imageCountIsLoading = useSelector(selectImagesCountLoading); - const workingImages = useSelector(selectWorkingImages); - - const dispatch = useDispatch(); - - const imageCount = useSelector(selectImagesCount); - - useEffect(() => { - if (deleteImagesByFilterLoading.isLoading && deleteImagesByFilterLoading.taskId) { - dispatch(fetchTask(deleteImagesByFilterLoading.taskId)); - } - }, [deleteImagesByFilterLoading, dispatch]); - - const handleDeleteImagesButtonClick = async () => { - const { isLoading, errors } = deleteImagesByFilterLoading; - const noErrors = !errors || errors.length === 0; - if (!isLoading && noErrors) { - if (imageCount <= IMAGE_QUERY_LIMITS[2]) { - dispatch(deleteImages(workingImages.map((i) => i._id))); - } else { - dispatch(deleteImagesByFilterTask({ filters })); - } - } - }; - - const [isSpinnerActive, setSpinner] = useState( - (deleteImagesByFilterLoading.isLoading && deleteImagesByFilterLoading.taskId) || - imageCountIsLoading.isLoading, - ); - - useEffect(() => { - setSpinner( - (deleteImagesByFilterLoading.isLoading && deleteImagesByFilterLoading.taskId) || - imageCountIsLoading.isLoading, - ); - }, [deleteImagesByFilterLoading, imageCountIsLoading.isLoading]); - - const handleCancelDelete = () => { - dispatch(setModalOpen(false)); - dispatch(setModalContent(null)); - }; - - return ( -
- {isSpinnerActive && ( - - - - )} - {imageCount === 0 ? ( - - We couldn't find any images that matched this set of filters. - - ) : ( - <> - -

- Do you wish to delete all images that match the current filters? This action will - delete {imageCount} {imageCount > 1 ? 'images' : 'image'} and cannot be undone. -

-
- - - - - - )} -
- ); -}; - -export default DeleteImagesByFilterModal; diff --git a/src/features/images/ImagesTableRow.jsx b/src/features/images/ImagesTableRow.jsx index 495213b1..4b8fa457 100644 --- a/src/features/images/ImagesTableRow.jsx +++ b/src/features/images/ImagesTableRow.jsx @@ -4,7 +4,12 @@ import { styled } from '../../theme/stitches.config.js'; import { selectUserUsername, selectUserCurrentRoles } from '../auth/authSlice.js'; import { hasRole, WRITE_OBJECTS_ROLES } from '../auth/roles.js'; import { setDeleteImagesAlertOpen } from './imagesSlice.js'; -import { toggleOpenLoupe, selectIsAddingLabel, addLabelStart, addLabelEnd } from '../loupe/loupeSlice.js'; +import { + toggleOpenLoupe, + selectIsAddingLabel, + addLabelStart, + addLabelEnd, +} from '../loupe/loupeSlice.js'; import { setFocus, setSelectedImageIndices, @@ -23,7 +28,14 @@ import { ContextMenuSeparator, } from '../../components/ContextMenu.jsx'; import CategorySelector from '../../components/CategorySelector.jsx'; -import { CheckIcon, Cross2Icon, LockOpen1Icon, Pencil1Icon, ValueNoneIcon, TrashIcon } from '@radix-ui/react-icons'; +import { + CheckIcon, + Cross2Icon, + LockOpen1Icon, + Pencil1Icon, + ValueNoneIcon, + TrashIcon, +} from '@radix-ui/react-icons'; // TODO: redundant component (exists in ImagesTable) const TableRow = styled('div', { @@ -168,12 +180,18 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) // NOTE: if this evaluation seems to cause performance issues // we can always not disable the buttons and perform these checks // in the handleMenuItemClick functions - const allObjectsLocked = selectedImages.every((img) => img.objects && img.objects.every((obj) => obj.locked)); - const allObjectsUnlocked = selectedImages.every((img) => img.objects && img.objects.every((obj) => !obj.locked)); + const allObjectsLocked = selectedImages.every( + (img) => img.objects && img.objects.every((obj) => obj.locked), + ); + const allObjectsUnlocked = selectedImages.every( + (img) => img.objects && img.objects.every((obj) => !obj.locked), + ); const hasRenderedObjects = selectedImages.some( (img) => img.objects && - img.objects.some((obj) => obj.labels.some((lbl) => lbl.validation === null || lbl.validation.validated)), + img.objects.some((obj) => + obj.labels.some((lbl) => lbl.validation === null || lbl.validation.validated), + ), ); // validate all labels @@ -185,7 +203,9 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) const unlockedObjects = image.objects.filter((obj) => !obj.locked); for (const object of unlockedObjects) { // find first non-invalidated label in array - const label = object.labels.find((lbl) => lbl.validation === null || lbl.validation.validated); + const label = object.labels.find( + (lbl) => lbl.validation === null || lbl.validation.validated, + ); labelsToValidate.push({ imgId: image._id, objId: object._id, @@ -213,7 +233,11 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) let objects = []; for (const image of selectedImages) { const objectsToUnlock = image.objects - .filter((obj) => obj.locked && obj.labels.some((lbl) => lbl.validation === null || lbl.validation.validated)) + .filter( + (obj) => + obj.locked && + obj.labels.some((lbl) => lbl.validation === null || lbl.validation.validated), + ) .map((obj) => ({ imgId: image._id, objId: obj._id })); objects = objects.concat(objectsToUnlock); @@ -257,7 +281,7 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) }; const handleDeleteImagesMenuItemClick = () => { - dispatch(setDeleteImagesAlertOpen(true)); + dispatch(setDeleteImagesAlertOpen(false)); }; return ( @@ -265,7 +289,11 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) {' '} {/* modal={false} is fix for pointer-events:none bug: https://github.com/radix-ui/primitives/issues/2416#issuecomment-1738294359 */} - handleRowClick(e, row.id)} selected={selected}> + handleRowClick(e, row.id)} + selected={selected} + > {row.cells.map((cell) => ( { @@ -180,8 +182,13 @@ export const imagesSlice = createSlice({ }, setDeleteImagesAlertOpen: (state, { payload }) => { - state.deleteImagesAlertOpen = payload; + state.deleteImagesAlertState.deleteImagesAlertOpen = true; + state.deleteImagesAlertState.deleteImagesAlertByFilter = payload; }, + setDeleteImagesAlertClose: (state) => { + state.deleteImagesAlertState.deleteImagesAlertOpen = false; + state.deleteImagesAlertState.deleteImagesAlertByFilter = null; + } }, }); @@ -206,6 +213,7 @@ export const { deleteImagesSuccess, deleteImagesError, setDeleteImagesAlertOpen, + setDeleteImagesAlertClose, } = imagesSlice.actions; // fetchImages thunk @@ -356,7 +364,7 @@ export const selectVisibleRows = (state) => state.images.visibleRows; export const selectPreFocusImage = (state) => state.images.preFocusImage; export const selectImageContextLoading = (state) => state.images.loadingStates.imageContext; export const selectImageContextErrors = (state) => state.images.loadingStates.imageContext.errors; -export const selectDeleteImagesAlertOpen = (state) => state.images.deleteImagesAlertOpen; +export const selectDeleteImagesAlertState = (state) => state.images.deleteImagesAlertState; // TODO: find a different place for this? export const selectRouterLocation = (state) => state.router.location; diff --git a/src/features/loupe/DeleteImagesAlert.jsx b/src/features/loupe/DeleteImagesAlert.jsx index 6eed0bb7..776c770c 100644 --- a/src/features/loupe/DeleteImagesAlert.jsx +++ b/src/features/loupe/DeleteImagesAlert.jsx @@ -1,10 +1,13 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { deleteImages, - setDeleteImagesAlertOpen, - selectDeleteImagesAlertOpen, + selectImagesCount, + selectImagesCountLoading, + selectDeleteImagesAlertState, + setDeleteImagesAlertClose, } from '../images/imagesSlice.js'; +import { selectActiveFilters } from '../filters/filtersSlice.js'; import { selectSelectedImages } from '../review/reviewSlice.js'; import { Alert, @@ -16,16 +19,21 @@ import { import Button from '../../components/Button.jsx'; import { red } from '@radix-ui/colors'; import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; -import { IMAGE_QUERY_LIMITS } from '../../config.js'; +import { IMAGE_DELETE_LIMIT } from '../../config.js'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; const DeleteImagesAlert = () => { const dispatch = useDispatch(); - const open = useSelector(selectDeleteImagesAlertOpen); + const alertState = useSelector(selectDeleteImagesAlertState); const selectedImages = useSelector(selectSelectedImages); const selectedImageIds = selectedImages.map((img) => img._id); const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const filters = useSelector(selectActiveFilters); + const imageCountIsLoading = useSelector(selectImagesCountLoading); + const imageCount = useSelector(selectImagesCount); + useEffect(() => { if (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) { dispatch(fetchTask(deleteImagesLoading.taskId)); @@ -33,25 +41,48 @@ const DeleteImagesAlert = () => { }, [deleteImagesLoading, dispatch]); const handleConfirmDelete = () => { - if (selectedImages.length <= IMAGE_QUERY_LIMITS[2]) { - dispatch(deleteImages(selectedImageIds)); + if (alertState.deleteImagesAlertByFilter) { + dispatch(deleteImagesTask([], filters, true)); } else { - dispatch(deleteImagesTask({ imageIds: selectedImageIds })); + if (selectedImages.length > IMAGE_DELETE_LIMIT) { + dispatch(deleteImagesTask(selectedImageIds, null, false)); + } else { + dispatch(deleteImages(selectedImageIds)); + } } }; const handleCancelDelete = () => { - dispatch(setDeleteImagesAlertOpen(false)); + dispatch(setDeleteImagesAlertClose()); }; + const [isSpinnerActive, setSpinner] = useState( + (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) || + (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading), + ); + + useEffect(() => { + setSpinner( + (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) || + (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading), + ); + }, [deleteImagesLoading, imageCountIsLoading.isLoading]); + + const filterText = `Are you sure you'd like to delete ${imageCount === 1 ? 'this image' : `these ${imageCount} images`}?`; + const selectionText = `Are you sure you'd like to delete ${selectedImages.length === 1 ? 'this image' : `these ${selectedImages.length} images`}?`; return ( - + - + + {isSpinnerActive && ( + + + + )} + - Are you sure you'd like to delete{' '} - {selectedImages.length > 1 ? `these ${selectedImages.length} images` : `this image`}? + {alertState.deleteImagesAlertByFilter ? filterText : selectionText}

This action can not be undone.

diff --git a/src/features/loupe/LoupeDropdown.jsx b/src/features/loupe/LoupeDropdown.jsx index c7ca0b8f..96538317 100644 --- a/src/features/loupe/LoupeDropdown.jsx +++ b/src/features/loupe/LoupeDropdown.jsx @@ -23,7 +23,7 @@ const LoupeDropdown = ({ image }) => { const dispatch = useDispatch(); const handleDeleteImageItemClick = () => { - dispatch(setDeleteImagesAlertOpen(true)); + dispatch(setDeleteImagesAlertOpen(false)); }; return ( diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 47a8b7f8..6819fcd0 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -10,7 +10,7 @@ import { } from '../projects/projectsSlice'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { setFocus, setSelectedImageIndices } from '../review/reviewSlice.js'; -import { fetchImages, setDeleteImagesAlertOpen } from '../images/imagesSlice.js'; +import { fetchImages, setDeleteImagesAlertClose } from '../images/imagesSlice.js'; const initialState = { loadingStates: { @@ -49,11 +49,6 @@ const initialState = { isLoading: false, errors: null }, - deleteImagesByFilter: { - taskId: null, - isLoading: false, - errors: null - }, }, imagesStats: null, annotationsExport: null, @@ -303,41 +298,6 @@ export const tasksSlice = createSlice({ state.loadingStates.deleteImages.errors.splice(index, 1); }, - // delete images by filter - - deleteImagesByFilterStart: (state) => { - let ls = state.loadingStates.deleteImagesByFilter; - ls.taskId = null; - ls.isLoading = true; - ls.errors = null; - }, - - deleteImagesByFilterUpdate: (state, { payload }) => { - state.loadingStates.deleteImagesByFilter.taskId = payload.taskId; - }, - - deleteImagesByFilterSuccess: (state) => { - let ls = state.loadingStates.deleteImagesByFilter; - ls.taskId = null; - ls.isLoading = false; - ls.errors = null; - }, - - deleteImagesByFilterFailure: (state, { payload }) => { - let ls = state.loadingStates.deleteImages; - ls.isLoading = false; - ls.errors = [payload.task.output.error]; - }, - - clearDeleteImagesByFilterTask: (state) => { - state.loadingStates.deleteImages = initialState.loadingStates.deleteImagesByFilter; - }, - - dismissDeleteImagesByFilterError: (state, { payload }) => { - const index = payload; - state.loadingStates.deleteImagesByFilter.taskId = null; - state.loadingStates.deleteImagesByFilter.errors.splice(index, 1); - }, }, }); @@ -385,13 +345,6 @@ export const { deleteImagesFailure, clearDeleteImagesTask, dismissDeleteImagesError, - - deleteImagesByFilterStart, - deleteImagesByFilterUpdate, - deleteImagesByFilterSuccess, - deleteImagesByFilterFailure, - clearDeleteImagesByFilterTask, - dismissDeleteImagesByFilterError, } = tasksSlice.actions; // fetchTask thunk @@ -465,7 +418,7 @@ export const fetchTask = (taskId) => { }, DeleteImagesByFilter: { COMPLETE: (res) => { - dispatch(deleteImagesByFilterSuccess()); + dispatch(deleteImagesSuccess()); dispatch( setFocus({ index: { image: null, object: null, label: null }, @@ -476,7 +429,7 @@ export const fetchTask = (taskId) => { dispatch(setModalContent(null)); dispatch(fetchImages(res.task.output.filters)); }, - FAIL: (res) => dispatch(deleteImagesByFilterFailure(res)) + FAIL: (res) => dispatch(deleteImagesFailure(res)) } }; @@ -625,7 +578,7 @@ export const updateCameraSerialNumber = (payload) => { }; // delete images thunk -export const deleteImagesTask = ({ imageIds }) => { +export const deleteImagesTask = (imageIds = [], filters = null, useFilters) => { return async (dispatch, getState) => { try { dispatch(deleteImagesStart()); @@ -633,13 +586,22 @@ export const deleteImagesTask = ({ imageIds }) => { const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); const projects = getState().projects.projects; const selectedProj = projects.find((proj) => proj.selected); - + let res = null; if (token && selectedProj) { - const res = await call({ - projId: selectedProj._id, - request: 'deleteImagesTask', - input: { imageIds }, - }); + if (useFilters) { + res = await call({ + projId: selectedProj._id, + request: 'deleteImagesByFilterTask', + input: { filters }, + }); + } else { + res = await call({ + projId: selectedProj._id, + request: 'deleteImagesTask', + input: { imageIds }, + }); + } + console.log('deleteImages - res: ', res); dispatch(deleteImagesUpdate({ taskId: res.deleteImagesTask._id })); dispatch( @@ -649,7 +611,7 @@ export const deleteImagesTask = ({ imageIds }) => { }), ); dispatch(setSelectedImageIndices([])); - dispatch(setDeleteImagesAlertOpen(false)); + dispatch(setDeleteImagesAlertClose()); } } catch (err) { @@ -658,32 +620,6 @@ export const deleteImagesTask = ({ imageIds }) => { }; }; -// delete images by filter thunk -export const deleteImagesByFilterTask = ({ filters }) => { - return async (dispatch, getState) => { - try { - dispatch(deleteImagesByFilterStart()); - const currentUser = await Auth.currentAuthenticatedUser(); - const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); - const projects = getState().projects.projects; - const selectedProj = projects.find((proj) => proj.selected); - console.log(filters); - - if (token && selectedProj) { - const res = await call({ - projId: selectedProj._id, - request: 'deleteImagesByFilterTask', - input: { filters }, - }); - console.log('deleteImagesByFilter - res: ', res); - dispatch(deleteImagesByFilterUpdate({ taskId: res.deleteImagesByFilterTask._id })); - } - } - catch (err) { - dispatch(deleteImagesByFilterFailure(err)); - } - }; -}; export const selectImagesStats = (state) => state.tasks.imagesStats; export const selectStatsLoading = (state) => state.tasks.loadingStates.stats; From a6987e296cd0c4a6f78e63df41ba8c2d7e1b05a5 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 4 Nov 2024 14:39:41 -0800 Subject: [PATCH 06/18] review comments --- src/features/auth/roles.js | 2 +- src/features/filters/FiltersPanelFooter.jsx | 9 +++++++-- src/features/review/reviewSlice.js | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/features/auth/roles.js b/src/features/auth/roles.js index 0ecaa74b..09e75cfd 100644 --- a/src/features/auth/roles.js +++ b/src/features/auth/roles.js @@ -13,7 +13,7 @@ export const WRITE_AUTOMATION_RULES_ROLES = [MANAGER]; export const WRITE_CAMERA_REGISTRATION_ROLES = [MANAGER]; export const WRITE_CAMERA_SERIAL_NUMBER_ROLES = [MANAGER]; export const QUERY_WITH_CUSTOM_FILTER = [MANAGER]; -export const DELETE_IMAGES = [MANAGER, MEMBER]; +export const DELETE_IMAGES_ROLES = [MANAGER, MEMBER]; export const MANAGE_USERS_ROLES = [MANAGER]; export const WRITE_PROJECT_ROLES = [MANAGER]; export const READ_COMMENT_ROLES = [MANAGER, MEMBER]; diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index 16f5b2a5..53e26e00 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -1,7 +1,12 @@ import React from 'react'; import { styled, keyframes } from '../../theme/stitches.config.js'; import { selectUserCurrentRoles } from '../auth/authSlice.js'; -import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES, WRITE_IMAGES_ROLES } from '../auth/roles.js'; +import { + hasRole, + READ_STATS_ROLES, + DELETE_IMAGES_ROLES, + EXPORT_DATA_ROLES, +} from '../auth/roles.js'; import { useDispatch, useSelector } from 'react-redux'; import { selectImagesCount, @@ -166,7 +171,7 @@ const FiltersPanelFooter = () => { )} - {hasRole(userRoles, WRITE_IMAGES_ROLES) && ( + {hasRole(userRoles, DELETE_IMAGES_ROLES) && ( diff --git a/src/features/review/reviewSlice.js b/src/features/review/reviewSlice.js index 61a3538e..f6220aa1 100644 --- a/src/features/review/reviewSlice.js +++ b/src/features/review/reviewSlice.js @@ -4,7 +4,7 @@ import { call } from '../../api'; import { findImage, findObject, findLabel, isImageReviewed } from '../../app/utils'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { getImagesSuccess, clearImages, deleteImagesSuccess } from '../images/imagesSlice'; -import { deleteImagesSuccess as deleteImageTaskSuccess } from '../tasks/tasksSlice'; +import { deleteImagesSuccess as deleteImagesTaskSuccess } from '../tasks/tasksSlice'; const initialState = { workingImages: [], @@ -209,7 +209,7 @@ export const reviewSlice = createSlice({ .addCase(deleteImagesSuccess, (state, { payload }) => { state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); }) - .addCase(deleteImageTaskSuccess, (state, { payload }) => { + .addCase(deleteImagesTaskSuccess, (state, { payload }) => { state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); }); }, From 13a106608d047ab3ef9e34b8d81c8e3b9f870b15 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 4 Nov 2024 16:50:22 -0800 Subject: [PATCH 07/18] closing alert after successful delete --- src/features/tasks/tasksSlice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 6819fcd0..3a922c06 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -425,10 +425,10 @@ export const fetchTask = (taskId) => { type: 'auto', }), ); - dispatch(setModalOpen(false)); - dispatch(setModalContent(null)); + dispatch(setDeleteImagesAlertClose()); dispatch(fetchImages(res.task.output.filters)); }, + FAIL: (res) => dispatch(deleteImagesFailure(res)) } }; From e667c901eff9932fae2f0e55a3ea9daa1069c1c1 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Tue, 5 Nov 2024 17:32:34 -0800 Subject: [PATCH 08/18] review comments --- src/components/ErrorToast.jsx | 5 ++++ src/features/filters/FiltersPanelFooter.jsx | 2 +- src/features/images/imagesSlice.js | 2 ++ src/features/loupe/DeleteImagesAlert.jsx | 5 ++-- src/features/tasks/tasksSlice.js | 31 ++++++++++----------- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/ErrorToast.jsx b/src/components/ErrorToast.jsx index f1983c79..a0853853 100644 --- a/src/components/ErrorToast.jsx +++ b/src/components/ErrorToast.jsx @@ -42,6 +42,8 @@ import { dismissDeploymentsError, selectCameraSerialNumberErrors, dismissCameraSerialNumberError, + selectDeleteImagesErrors, + dismissDeleteImagesError, } from '../features/tasks/tasksSlice'; import { selectRedriveBatchErrors, @@ -74,6 +76,7 @@ const ErrorToast = () => { const manageLabelsErrors = useSelector(selectManageLabelsErrors); const uploadErrors = useSelector(selectUploadErrors); const cameraSerialNumberErrors = useSelector(selectCameraSerialNumberErrors); + const deleteImagesErrors = useSelector(selectDeleteImagesErrors); const enrichedErrors = [ enrichErrors(labelsErrors, 'Label Error', 'labels'), @@ -98,6 +101,7 @@ const ErrorToast = () => { 'Error Updating Camera Serial Number', 'cameraSerialNumber', ), + enrichErrors(deleteImagesErrors, 'Error Deleting Images', 'deleteImages'), ]; const errors = enrichedErrors.reduce( @@ -161,6 +165,7 @@ const dismissErrorActions = { manageLabels: (i) => dismissManageLabelsError(i), upload: (i) => dismissUploadError(i), cameraSerialNumber: (i) => dismissCameraSerialNumberError(i), + deleteImagesError: (i) => dismissDeleteImagesError(i), }; function enrichErrors(errors, title, entity) { diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index 53e26e00..bb899c48 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -187,7 +187,7 @@ const FiltersPanelFooter = () => { - Delete all images shown + Delete all filtered images diff --git a/src/features/images/imagesSlice.js b/src/features/images/imagesSlice.js index cd948e2a..e32eeefe 100644 --- a/src/features/images/imagesSlice.js +++ b/src/features/images/imagesSlice.js @@ -345,9 +345,11 @@ export const deleteImages = (imageIds) => async (dispatch, getState) => { ); dispatch(setSelectedImageIndices([])); dispatch(deleteImagesSuccess(imageIds)); + dispatch(setDeleteImagesAlertClose()) } catch (err) { console.log(`error attempting to delete image: `, err); dispatch(deleteImagesError(err)); + dispatch(setDeleteImagesAlertClose()) } }; diff --git a/src/features/loupe/DeleteImagesAlert.jsx b/src/features/loupe/DeleteImagesAlert.jsx index 776c770c..21005aff 100644 --- a/src/features/loupe/DeleteImagesAlert.jsx +++ b/src/features/loupe/DeleteImagesAlert.jsx @@ -73,14 +73,13 @@ const DeleteImagesAlert = () => { return ( - + + {isSpinnerActive && ( )} - - {alertState.deleteImagesAlertByFilter ? filterText : selectionText} diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 3a922c06..6e85f6a2 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -413,18 +413,27 @@ export const fetchTask = (taskId) => { DeleteImages: { COMPLETE: (res) => { dispatch(deleteImagesSuccess(res.task.output.imageIds)); + dispatch( + setFocus({ + index: { image: null, object: null, label: null }, + type: 'auto', + }), + ); + dispatch(setSelectedImageIndices([])); + dispatch(setDeleteImagesAlertClose()); }, FAIL: (res) => dispatch(deleteImagesFailure(res)) }, DeleteImagesByFilter: { COMPLETE: (res) => { - dispatch(deleteImagesSuccess()); + dispatch(deleteImagesSuccess([])); dispatch( setFocus({ index: { image: null, object: null, label: null }, type: 'auto', }), ); + dispatch(setSelectedImageIndices([])); dispatch(setDeleteImagesAlertClose()); dispatch(fetchImages(res.task.output.filters)); }, @@ -586,32 +595,22 @@ export const deleteImagesTask = (imageIds = [], filters = null, useFilters) => { const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); const projects = getState().projects.projects; const selectedProj = projects.find((proj) => proj.selected); - let res = null; if (token && selectedProj) { if (useFilters) { - res = await call({ + const res = await call({ projId: selectedProj._id, request: 'deleteImagesByFilterTask', input: { filters }, }); + dispatch(deleteImagesUpdate({ taskId: res.deleteImagesByFilterTask._id })); } else { - res = await call({ + const res = await call({ projId: selectedProj._id, request: 'deleteImagesTask', input: { imageIds }, }); + dispatch(deleteImagesUpdate({ taskId: res.deleteImagesTask._id })); } - - console.log('deleteImages - res: ', res); - dispatch(deleteImagesUpdate({ taskId: res.deleteImagesTask._id })); - dispatch( - setFocus({ - index: { image: null, object: null, label: null }, - type: 'auto', - }), - ); - dispatch(setSelectedImageIndices([])); - dispatch(setDeleteImagesAlertClose()); } } catch (err) { @@ -639,6 +638,6 @@ export const selectCameraSerialNumberLoading = (state) => export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImages; -export const selectDeleteImagesByFilterLoading = (state) => state.tasks.loadingStates.deleteImagesByFilter; +export const selectDeleteImagesErrors = (state) => state.tasks.loadingStates.deleteImages.errors; export default tasksSlice.reducer; From 1868c61798bfb94f605d86c2868ce981b8d8826d Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 6 Nov 2024 23:34:41 -0800 Subject: [PATCH 09/18] adding dropwdown menu --- src/features/filters/FiltersPanelFooter.jsx | 70 +++---------------- .../filters/FiltersPanelFooterDropdown.jsx | 57 +++++++++++++++ 2 files changed, 66 insertions(+), 61 deletions(-) create mode 100644 src/features/filters/FiltersPanelFooterDropdown.jsx diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index bb899c48..9a345f41 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -1,18 +1,9 @@ import React from 'react'; import { styled, keyframes } from '../../theme/stitches.config.js'; import { selectUserCurrentRoles } from '../auth/authSlice.js'; -import { - hasRole, - READ_STATS_ROLES, - DELETE_IMAGES_ROLES, - EXPORT_DATA_ROLES, -} from '../auth/roles.js'; +import { hasRole, READ_STATS_ROLES } from '../auth/roles.js'; import { useDispatch, useSelector } from 'react-redux'; -import { - selectImagesCount, - selectImagesCountLoading, - setDeleteImagesAlertOpen, -} from '../images/imagesSlice.js'; +import { selectImagesCount, selectImagesCountLoading } from '../images/imagesSlice.js'; import { selectModalOpen, selectSelectedProject, @@ -21,7 +12,7 @@ import { fetchProjects, } from '../projects/projectsSlice.js'; import { toggleOpenLoupe } from '../loupe/loupeSlice.js'; -import { InfoCircledIcon, DownloadIcon, SymbolIcon, TrashIcon } from '@radix-ui/react-icons'; +import { InfoCircledIcon, SymbolIcon } from '@radix-ui/react-icons'; import IconButton from '../../components/IconButton.jsx'; import { Tooltip, @@ -29,6 +20,7 @@ import { TooltipArrow, TooltipTrigger, } from '../../components/Tooltip.jsx'; +import FiltersPanelFooterDropdown from './FiltersPanelFooterDropdown.jsx'; const RefreshButton = styled('div', { height: '100%', @@ -46,15 +38,7 @@ const InfoButton = styled('div', { padding: '0 $1', }); -const ExportCSVButton = styled('div', { - height: '100%', - borderLeft: '1px solid $border', - display: 'flex', - alignItems: 'center', - padding: '0 $1', -}); - -const DeleteImagesButton = styled('div', { +const DropDownButton = styled('div', { height: '100%', borderLeft: '1px solid $border', display: 'flex', @@ -118,6 +102,7 @@ const FiltersPanelFooter = () => { }; const handleModalToggle = (content) => { + console.log(content); dispatch(setModalOpen(!modalOpen)); dispatch(setModalContent(content)); }; @@ -152,46 +137,6 @@ const FiltersPanelFooter = () => { )} - {hasRole(userRoles, EXPORT_DATA_ROLES) && ( - - - - handleModalToggle('export-modal')} - > - - - - - - Export data - - - - )} - {hasRole(userRoles, DELETE_IMAGES_ROLES) && ( - - - - { - dispatch(setDeleteImagesAlertOpen(true)); - }} - > - - - - - - Delete all filtered images - - - - )} @@ -205,6 +150,9 @@ const FiltersPanelFooter = () => { + + + ); }; diff --git a/src/features/filters/FiltersPanelFooterDropdown.jsx b/src/features/filters/FiltersPanelFooterDropdown.jsx new file mode 100644 index 00000000..691cbc19 --- /dev/null +++ b/src/features/filters/FiltersPanelFooterDropdown.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { styled } from '../../theme/stitches.config'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuArrow, +} from '../../components/Dropdown.jsx'; +import { selectUserCurrentRoles } from '../auth/authSlice.js'; +import { hasRole, DELETE_IMAGES_ROLES, EXPORT_DATA_ROLES } from '../auth/roles.js'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; +import { setDeleteImagesAlertOpen } from '../images/imagesSlice'; + +const StyledDropdownMenuTrigger = styled(DropdownMenuTrigger, { + height: '100%', + width: '40px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + border: 'none', + backgroundColor: 'transparent', + padding: '0', +}); + +const FiltersPanelFooterDropdown = (props) => { + const dispatch = useDispatch(); + const userRoles = useSelector(selectUserCurrentRoles); + + const handleDeleteImageItemClick = () => { + dispatch(setDeleteImagesAlertOpen(true)); + }; + + return ( + + + + + + {hasRole(userRoles, EXPORT_DATA_ROLES) && ( + props.handleModalToggle('export-modal')}> + Export data + + )} + {hasRole(userRoles, DELETE_IMAGES_ROLES) && ( + + Delete Filtered Images + + )} + + + + ); +}; + +export default FiltersPanelFooterDropdown; From 2056788beef96c31633f19f40160ff3fa2206787 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Thu, 7 Nov 2024 11:05:28 -0800 Subject: [PATCH 10/18] clean up --- src/features/filters/FiltersPanelFooter.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index 9a345f41..b1643736 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -102,7 +102,6 @@ const FiltersPanelFooter = () => { }; const handleModalToggle = (content) => { - console.log(content); dispatch(setModalOpen(!modalOpen)); dispatch(setModalContent(content)); }; From 19ef7080a6a13ba024c35017eac5b1d8ecb6b8ed Mon Sep 17 00:00:00 2001 From: jue-henry Date: Thu, 7 Nov 2024 12:15:37 -0800 Subject: [PATCH 11/18] addressing warnings --- src/features/loupe/BoundingBox.jsx | 50 +++++++++++++++++----------- src/features/loupe/FullSizeImage.jsx | 6 +++- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/features/loupe/BoundingBox.jsx b/src/features/loupe/BoundingBox.jsx index 7c5c3940..1a784ec0 100644 --- a/src/features/loupe/BoundingBox.jsx +++ b/src/features/loupe/BoundingBox.jsx @@ -15,7 +15,12 @@ import { ContextMenuSeparator, ContextMenuItemIconLeft, } from '../../components/ContextMenu'; -import { bboxUpdated, labelsValidated, setFocus, objectsManuallyUnlocked } from '../review/reviewSlice'; +import { + bboxUpdated, + labelsValidated, + setFocus, + objectsManuallyUnlocked, +} from '../review/reviewSlice'; import { addLabelStart } from './loupeSlice'; import BoundingBoxLabel from './BoundingBoxLabel'; import { absToRel, relToAbs } from '../../app/utils'; @@ -117,6 +122,7 @@ const BoundingBox = ({ imgId, imgDims, object, objectIndex, focusIndex, setTempO const catSelectorRef = useRef(null); const focusRef = useRef(null); const dispatch = useDispatch(); + const dragRef = useRef(null); // track whether the object is focused const objectFocused = object.isTemp || focusIndex.object === objectIndex; @@ -259,8 +265,10 @@ const BoundingBox = ({ imgId, imgDims, object, objectIndex, focusIndex, setTempO onDrag={onDrag} onStop={onDragEnd} disabled={!isAuthorized || object.locked} + nodeRef={dragRef} > - {label && ( - 30 ? 'top' : 'bottom'} - horizontalPos={imgDims.width - left - width < 75 ? 'right' : 'left'} - ref={catSelectorRef} - isAuthorized={isAuthorized} - username={username} - /> - )} - + <> + {label && ( + 30 ? 'top' : 'bottom'} + horizontalPos={imgDims.width - left - width < 75 ? 'right' : 'left'} + ref={catSelectorRef} + isAuthorized={isAuthorized} + username={username} + /> + )} + + diff --git a/src/features/loupe/FullSizeImage.jsx b/src/features/loupe/FullSizeImage.jsx index 045c7bfb..6fe361cf 100644 --- a/src/features/loupe/FullSizeImage.jsx +++ b/src/features/loupe/FullSizeImage.jsx @@ -100,7 +100,11 @@ const FullSizeImage = ({ workingImages, image, focusIndex }) => { ); })} {isDrawingBbox && ( - + )} {/*{!imgLoaded && From 10818dbbdb8548b430f778e67860675533aeebff Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Fri, 8 Nov 2024 07:28:47 -1000 Subject: [PATCH 12/18] Restrict delete images roles to managers --- src/features/auth/roles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/roles.js b/src/features/auth/roles.js index 09e75cfd..1b3b8420 100644 --- a/src/features/auth/roles.js +++ b/src/features/auth/roles.js @@ -13,7 +13,7 @@ export const WRITE_AUTOMATION_RULES_ROLES = [MANAGER]; export const WRITE_CAMERA_REGISTRATION_ROLES = [MANAGER]; export const WRITE_CAMERA_SERIAL_NUMBER_ROLES = [MANAGER]; export const QUERY_WITH_CUSTOM_FILTER = [MANAGER]; -export const DELETE_IMAGES_ROLES = [MANAGER, MEMBER]; +export const DELETE_IMAGES_ROLES = [MANAGER]; export const MANAGE_USERS_ROLES = [MANAGER]; export const WRITE_PROJECT_ROLES = [MANAGER]; export const READ_COMMENT_ROLES = [MANAGER, MEMBER]; From c302a766167cdc4c3e19ca0c9bbeda1e2718919b Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Fri, 8 Nov 2024 08:18:37 -1000 Subject: [PATCH 13/18] Update filters panel menu item text --- src/features/filters/FiltersPanelFooterDropdown.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/filters/FiltersPanelFooterDropdown.jsx b/src/features/filters/FiltersPanelFooterDropdown.jsx index 691cbc19..9f61b863 100644 --- a/src/features/filters/FiltersPanelFooterDropdown.jsx +++ b/src/features/filters/FiltersPanelFooterDropdown.jsx @@ -40,12 +40,12 @@ const FiltersPanelFooterDropdown = (props) => { {hasRole(userRoles, EXPORT_DATA_ROLES) && ( props.handleModalToggle('export-modal')}> - Export data + Export filtered data )} {hasRole(userRoles, DELETE_IMAGES_ROLES) && ( - Delete Filtered Images + Delete filtered images )} From d95cfc47c37f82dadfea42028bff15c97b7fb6a0 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 11 Nov 2024 14:44:09 -0800 Subject: [PATCH 14/18] fixing bug --- src/features/tasks/tasksSlice.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 6e85f6a2..756db2d7 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -412,21 +412,23 @@ export const fetchTask = (taskId) => { }, DeleteImages: { COMPLETE: (res) => { - dispatch(deleteImagesSuccess(res.task.output.imageIds)); - dispatch( - setFocus({ - index: { image: null, object: null, label: null }, - type: 'auto', - }), - ); - dispatch(setSelectedImageIndices([])); - dispatch(setDeleteImagesAlertClose()); + if (res.task._id === getState().tasks.loadingStates.deleteImages.taskId) { + console.log('deleteImages - res: ', res); + dispatch( + setFocus({ + index: { image: null, object: null, label: null }, + type: 'auto', + }), + ); + dispatch(setSelectedImageIndices([])); + dispatch(deleteImagesSuccess(res.task.output.imageIds)); + dispatch(setDeleteImagesAlertClose()); + } }, FAIL: (res) => dispatch(deleteImagesFailure(res)) }, DeleteImagesByFilter: { COMPLETE: (res) => { - dispatch(deleteImagesSuccess([])); dispatch( setFocus({ index: { image: null, object: null, label: null }, @@ -434,6 +436,7 @@ export const fetchTask = (taskId) => { }), ); dispatch(setSelectedImageIndices([])); + dispatch(deleteImagesSuccess([])); dispatch(setDeleteImagesAlertClose()); dispatch(fetchImages(res.task.output.filters)); }, From d691af631e605f18ea5901252f08775d0f2727d9 Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Wed, 13 Nov 2024 10:19:07 -0800 Subject: [PATCH 15/18] Display spinner during sync deletion --- src/features/images/imagesSlice.js | 11 ++++++--- src/features/loupe/BoundingBox.jsx | 1 - src/features/loupe/DeleteImagesAlert.jsx | 31 ++++++++++-------------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/features/images/imagesSlice.js b/src/features/images/imagesSlice.js index e32eeefe..d33a8cc8 100644 --- a/src/features/images/imagesSlice.js +++ b/src/features/images/imagesSlice.js @@ -165,6 +165,10 @@ export const imagesSlice = createSlice({ state.loadingStates.imageContext.errors.splice(index, 1); }, + // NOTE: the following deleteImages actions are for deleting small + // numbers of images synchronously. For deleting large numbers of images, + // use deleteImagesTask from the taskSlice + deleteImagesStart: (state) => { state.loadingStates.images.isLoading = true; state.loadingStates.images.operation = 'deleting'; @@ -185,10 +189,11 @@ export const imagesSlice = createSlice({ state.deleteImagesAlertState.deleteImagesAlertOpen = true; state.deleteImagesAlertState.deleteImagesAlertByFilter = payload; }, + setDeleteImagesAlertClose: (state) => { state.deleteImagesAlertState.deleteImagesAlertOpen = false; state.deleteImagesAlertState.deleteImagesAlertByFilter = null; - } + }, }, }); @@ -345,11 +350,11 @@ export const deleteImages = (imageIds) => async (dispatch, getState) => { ); dispatch(setSelectedImageIndices([])); dispatch(deleteImagesSuccess(imageIds)); - dispatch(setDeleteImagesAlertClose()) + dispatch(setDeleteImagesAlertClose()); } catch (err) { console.log(`error attempting to delete image: `, err); dispatch(deleteImagesError(err)); - dispatch(setDeleteImagesAlertClose()) + dispatch(setDeleteImagesAlertClose()); } }; diff --git a/src/features/loupe/BoundingBox.jsx b/src/features/loupe/BoundingBox.jsx index 1a784ec0..b5714cda 100644 --- a/src/features/loupe/BoundingBox.jsx +++ b/src/features/loupe/BoundingBox.jsx @@ -243,7 +243,6 @@ const BoundingBox = ({ imgId, imgDims, object, objectIndex, focusIndex, setTempO // focus to shift to the react-select category selector component. // see https://github.com/radix-ui/primitives/issues/1446 focusRef.current = catSelectorRef.current; - console.log('focusRef.current: ', focusRef.current); const newIndex = { image: focusIndex.image, object: objectIndex, label: null }; dispatch(setFocus({ index: newIndex, type: 'manual' })); dispatch(addLabelStart('from-object')); diff --git a/src/features/loupe/DeleteImagesAlert.jsx b/src/features/loupe/DeleteImagesAlert.jsx index 21005aff..e2e7ff20 100644 --- a/src/features/loupe/DeleteImagesAlert.jsx +++ b/src/features/loupe/DeleteImagesAlert.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { deleteImages, @@ -6,6 +6,7 @@ import { selectImagesCountLoading, selectDeleteImagesAlertState, setDeleteImagesAlertClose, + selectImagesLoading, } from '../images/imagesSlice.js'; import { selectActiveFilters } from '../filters/filtersSlice.js'; import { selectSelectedImages } from '../review/reviewSlice.js'; @@ -28,17 +29,18 @@ const DeleteImagesAlert = () => { const selectedImages = useSelector(selectSelectedImages); const selectedImageIds = selectedImages.map((img) => img._id); - const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const imagesLoading = useSelector(selectImagesLoading); + const deleteImagesTaskLoading = useSelector(selectDeleteImagesLoading); const filters = useSelector(selectActiveFilters); const imageCountIsLoading = useSelector(selectImagesCountLoading); const imageCount = useSelector(selectImagesCount); useEffect(() => { - if (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) { - dispatch(fetchTask(deleteImagesLoading.taskId)); + if (deleteImagesTaskLoading.isLoading && deleteImagesTaskLoading.taskId) { + dispatch(fetchTask(deleteImagesTaskLoading.taskId)); } - }, [deleteImagesLoading, dispatch]); + }, [deleteImagesTaskLoading, dispatch]); const handleConfirmDelete = () => { if (alertState.deleteImagesAlertByFilter) { @@ -56,20 +58,13 @@ const DeleteImagesAlert = () => { dispatch(setDeleteImagesAlertClose()); }; - const [isSpinnerActive, setSpinner] = useState( - (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) || - (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading), - ); - - useEffect(() => { - setSpinner( - (deleteImagesLoading.isLoading && deleteImagesLoading.taskId) || - (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading), - ); - }, [deleteImagesLoading, imageCountIsLoading.isLoading]); + const isSpinnerActive = + (deleteImagesTaskLoading.isLoading && deleteImagesTaskLoading.taskId) || + (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading) || + imagesLoading.isLoading; - const filterText = `Are you sure you'd like to delete ${imageCount === 1 ? 'this image' : `these ${imageCount} images`}?`; - const selectionText = `Are you sure you'd like to delete ${selectedImages.length === 1 ? 'this image' : `these ${selectedImages.length} images`}?`; + const filterText = `Are you sure you'd like to delete ${imageCount === 1 ? 'this image' : `these ${imageCount && imageCount.toLocaleString()} images`}?`; + const selectionText = `Are you sure you'd like to delete ${selectedImages.length === 1 ? 'this image' : `these ${selectedImages && selectedImages.length.toLocaleString()} images`}?`; return ( From 10ab7c9e8a0967bd0c4decefcdc1b2c1d5b99e10 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 13 Nov 2024 10:50:42 -0800 Subject: [PATCH 16/18] consolidating alert open/close, clarifying deleteImags thunk function, reviewing bounding box warning fixes --- .../filters/FiltersPanelFooterDropdown.jsx | 4 ++-- src/features/images/ImagesTableRow.jsx | 4 ++-- src/features/images/imagesSlice.js | 24 +++++++++---------- src/features/loupe/BoundingBox.jsx | 3 --- src/features/loupe/DeleteImagesAlert.jsx | 8 +++---- src/features/loupe/LoupeDropdown.jsx | 4 ++-- src/features/tasks/tasksSlice.js | 15 ++++++++---- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/features/filters/FiltersPanelFooterDropdown.jsx b/src/features/filters/FiltersPanelFooterDropdown.jsx index 9f61b863..a970ecaf 100644 --- a/src/features/filters/FiltersPanelFooterDropdown.jsx +++ b/src/features/filters/FiltersPanelFooterDropdown.jsx @@ -11,7 +11,7 @@ import { import { selectUserCurrentRoles } from '../auth/authSlice.js'; import { hasRole, DELETE_IMAGES_ROLES, EXPORT_DATA_ROLES } from '../auth/roles.js'; import { DotsHorizontalIcon } from '@radix-ui/react-icons'; -import { setDeleteImagesAlertOpen } from '../images/imagesSlice'; +import { setDeleteImagesAlertStatus } from '../images/imagesSlice'; const StyledDropdownMenuTrigger = styled(DropdownMenuTrigger, { height: '100%', @@ -29,7 +29,7 @@ const FiltersPanelFooterDropdown = (props) => { const userRoles = useSelector(selectUserCurrentRoles); const handleDeleteImageItemClick = () => { - dispatch(setDeleteImagesAlertOpen(true)); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: true })); }; return ( diff --git a/src/features/images/ImagesTableRow.jsx b/src/features/images/ImagesTableRow.jsx index 4b8fa457..694638eb 100644 --- a/src/features/images/ImagesTableRow.jsx +++ b/src/features/images/ImagesTableRow.jsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { styled } from '../../theme/stitches.config.js'; import { selectUserUsername, selectUserCurrentRoles } from '../auth/authSlice.js'; import { hasRole, WRITE_OBJECTS_ROLES } from '../auth/roles.js'; -import { setDeleteImagesAlertOpen } from './imagesSlice.js'; +import { setDeleteImagesAlertStatus } from './imagesSlice.js'; import { toggleOpenLoupe, selectIsAddingLabel, @@ -281,7 +281,7 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) }; const handleDeleteImagesMenuItemClick = () => { - dispatch(setDeleteImagesAlertOpen(false)); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: false })); }; return ( diff --git a/src/features/images/imagesSlice.js b/src/features/images/imagesSlice.js index d33a8cc8..47eea157 100644 --- a/src/features/images/imagesSlice.js +++ b/src/features/images/imagesSlice.js @@ -185,14 +185,15 @@ export const imagesSlice = createSlice({ state.loadingStates.images.errors = payload; }, - setDeleteImagesAlertOpen: (state, { payload }) => { - state.deleteImagesAlertState.deleteImagesAlertOpen = true; - state.deleteImagesAlertState.deleteImagesAlertByFilter = payload; - }, - - setDeleteImagesAlertClose: (state) => { - state.deleteImagesAlertState.deleteImagesAlertOpen = false; - state.deleteImagesAlertState.deleteImagesAlertByFilter = null; + setDeleteImagesAlertStatus: (state, { openStatus, deleteImagesByFilter }) => { + if (openStatus) { + state.deleteImagesAlertState.deleteImagesAlertOpen = openStatus; + state.deleteImagesAlertState.deleteImagesAlertByFilter = deleteImagesByFilter; + } + else { + state.deleteImagesAlertState.deleteImagesAlertOpen = openStatus; + state.deleteImagesAlertState.deleteImagesAlertByFilter = null; + } }, }, }); @@ -217,8 +218,7 @@ export const { deleteImagesStart, deleteImagesSuccess, deleteImagesError, - setDeleteImagesAlertOpen, - setDeleteImagesAlertClose, + setDeleteImagesAlertStatus, } = imagesSlice.actions; // fetchImages thunk @@ -350,11 +350,11 @@ export const deleteImages = (imageIds) => async (dispatch, getState) => { ); dispatch(setSelectedImageIndices([])); dispatch(deleteImagesSuccess(imageIds)); - dispatch(setDeleteImagesAlertClose()); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); } catch (err) { console.log(`error attempting to delete image: `, err); dispatch(deleteImagesError(err)); - dispatch(setDeleteImagesAlertClose()); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); } }; diff --git a/src/features/loupe/BoundingBox.jsx b/src/features/loupe/BoundingBox.jsx index b5714cda..20653ea1 100644 --- a/src/features/loupe/BoundingBox.jsx +++ b/src/features/loupe/BoundingBox.jsx @@ -122,7 +122,6 @@ const BoundingBox = ({ imgId, imgDims, object, objectIndex, focusIndex, setTempO const catSelectorRef = useRef(null); const focusRef = useRef(null); const dispatch = useDispatch(); - const dragRef = useRef(null); // track whether the object is focused const objectFocused = object.isTemp || focusIndex.object === objectIndex; @@ -264,10 +263,8 @@ const BoundingBox = ({ imgId, imgDims, object, objectIndex, focusIndex, setTempO onDrag={onDrag} onStop={onDragEnd} disabled={!isAuthorized || object.locked} - nodeRef={dragRef} > { const handleConfirmDelete = () => { if (alertState.deleteImagesAlertByFilter) { - dispatch(deleteImagesTask([], filters, true)); + dispatch(deleteImagesTask({ imageIds: [], filters: filters })); } else { if (selectedImages.length > IMAGE_DELETE_LIMIT) { - dispatch(deleteImagesTask(selectedImageIds, null, false)); + dispatch(deleteImagesTask({ imageIds: selectedImageIds, filters: null })); } else { dispatch(deleteImages(selectedImageIds)); } @@ -55,7 +55,7 @@ const DeleteImagesAlert = () => { }; const handleCancelDelete = () => { - dispatch(setDeleteImagesAlertClose()); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); }; const isSpinnerActive = diff --git a/src/features/loupe/LoupeDropdown.jsx b/src/features/loupe/LoupeDropdown.jsx index 96538317..e9e8d029 100644 --- a/src/features/loupe/LoupeDropdown.jsx +++ b/src/features/loupe/LoupeDropdown.jsx @@ -11,7 +11,7 @@ import { import IconButton from '../../components/IconButton.jsx'; import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import DeleteImagesAlert from './DeleteImagesAlert.jsx'; -import { setDeleteImagesAlertOpen } from '../images/imagesSlice'; +import { setDeleteImagesAlertStatus } from '../images/imagesSlice'; const StyledDropdownMenuTrigger = styled(DropdownMenuTrigger, { position: 'absolute', @@ -23,7 +23,7 @@ const LoupeDropdown = ({ image }) => { const dispatch = useDispatch(); const handleDeleteImageItemClick = () => { - dispatch(setDeleteImagesAlertOpen(false)); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: false })); }; return ( diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 756db2d7..7405e789 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -10,7 +10,7 @@ import { } from '../projects/projectsSlice'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { setFocus, setSelectedImageIndices } from '../review/reviewSlice.js'; -import { fetchImages, setDeleteImagesAlertClose } from '../images/imagesSlice.js'; +import { fetchImages, setDeleteImagesAlertStatus } from '../images/imagesSlice.js'; const initialState = { loadingStates: { @@ -422,7 +422,7 @@ export const fetchTask = (taskId) => { ); dispatch(setSelectedImageIndices([])); dispatch(deleteImagesSuccess(res.task.output.imageIds)); - dispatch(setDeleteImagesAlertClose()); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); } }, FAIL: (res) => dispatch(deleteImagesFailure(res)) @@ -437,7 +437,7 @@ export const fetchTask = (taskId) => { ); dispatch(setSelectedImageIndices([])); dispatch(deleteImagesSuccess([])); - dispatch(setDeleteImagesAlertClose()); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); dispatch(fetchImages(res.task.output.filters)); }, @@ -590,7 +590,12 @@ export const updateCameraSerialNumber = (payload) => { }; // delete images thunk -export const deleteImagesTask = (imageIds = [], filters = null, useFilters) => { +export const deleteImagesTask = ({ imageIds = [], filters = null }) => { + /** + * Deletes images by either imageIds or by filters, one argument has to be populated and filters take precedence + * @param {Array} imageIds - array of image ids to delete + * @param {Object} filters - filters to delete images by + */ return async (dispatch, getState) => { try { dispatch(deleteImagesStart()); @@ -599,7 +604,7 @@ export const deleteImagesTask = (imageIds = [], filters = null, useFilters) => { const projects = getState().projects.projects; const selectedProj = projects.find((proj) => proj.selected); if (token && selectedProj) { - if (useFilters) { + if (filters !== null) { const res = await call({ projId: selectedProj._id, request: 'deleteImagesByFilterTask', From ab0843a8f3167d9f0916207d2152ab2144dc3f62 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 13 Nov 2024 11:26:28 -0800 Subject: [PATCH 17/18] fixing deleteImagesByFilter --- src/features/filters/FiltersPanelFooterDropdown.jsx | 2 +- src/features/images/ImagesTableRow.jsx | 2 +- src/features/images/imagesSlice.js | 2 +- src/features/loupe/LoupeDropdown.jsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/filters/FiltersPanelFooterDropdown.jsx b/src/features/filters/FiltersPanelFooterDropdown.jsx index a970ecaf..469e0b8b 100644 --- a/src/features/filters/FiltersPanelFooterDropdown.jsx +++ b/src/features/filters/FiltersPanelFooterDropdown.jsx @@ -29,7 +29,7 @@ const FiltersPanelFooterDropdown = (props) => { const userRoles = useSelector(selectUserCurrentRoles); const handleDeleteImageItemClick = () => { - dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: true })); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteImagesByFilter: true })); }; return ( diff --git a/src/features/images/ImagesTableRow.jsx b/src/features/images/ImagesTableRow.jsx index 694638eb..2ac5e002 100644 --- a/src/features/images/ImagesTableRow.jsx +++ b/src/features/images/ImagesTableRow.jsx @@ -281,7 +281,7 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices }) }; const handleDeleteImagesMenuItemClick = () => { - dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: false })); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteImagesByFilter: false })); }; return ( diff --git a/src/features/images/imagesSlice.js b/src/features/images/imagesSlice.js index 47eea157..ea10446c 100644 --- a/src/features/images/imagesSlice.js +++ b/src/features/images/imagesSlice.js @@ -185,7 +185,7 @@ export const imagesSlice = createSlice({ state.loadingStates.images.errors = payload; }, - setDeleteImagesAlertStatus: (state, { openStatus, deleteImagesByFilter }) => { + setDeleteImagesAlertStatus: (state, { payload: { openStatus, deleteImagesByFilter } }) => { if (openStatus) { state.deleteImagesAlertState.deleteImagesAlertOpen = openStatus; state.deleteImagesAlertState.deleteImagesAlertByFilter = deleteImagesByFilter; diff --git a/src/features/loupe/LoupeDropdown.jsx b/src/features/loupe/LoupeDropdown.jsx index e9e8d029..9265fcec 100644 --- a/src/features/loupe/LoupeDropdown.jsx +++ b/src/features/loupe/LoupeDropdown.jsx @@ -23,7 +23,7 @@ const LoupeDropdown = ({ image }) => { const dispatch = useDispatch(); const handleDeleteImageItemClick = () => { - dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteByFilter: false })); + dispatch(setDeleteImagesAlertStatus({ openStatus: true, deleteImagesByFilter: false })); }; return ( From 260a7b25c8bb45994e639c2d27de01f03b1b316d Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 13 Nov 2024 12:42:37 -0800 Subject: [PATCH 18/18] update images after image deletion tasks --- src/features/review/reviewSlice.js | 4 ---- src/features/tasks/tasksSlice.js | 27 ++++++++++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/features/review/reviewSlice.js b/src/features/review/reviewSlice.js index f6220aa1..c8837514 100644 --- a/src/features/review/reviewSlice.js +++ b/src/features/review/reviewSlice.js @@ -4,7 +4,6 @@ import { call } from '../../api'; import { findImage, findObject, findLabel, isImageReviewed } from '../../app/utils'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { getImagesSuccess, clearImages, deleteImagesSuccess } from '../images/imagesSlice'; -import { deleteImagesSuccess as deleteImagesTaskSuccess } from '../tasks/tasksSlice'; const initialState = { workingImages: [], @@ -209,9 +208,6 @@ export const reviewSlice = createSlice({ .addCase(deleteImagesSuccess, (state, { payload }) => { state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); }) - .addCase(deleteImagesTaskSuccess, (state, { payload }) => { - state.workingImages = state.workingImages.filter(({ _id }) => !payload.includes(_id)); - }); }, }); diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 7405e789..9762d7a7 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -10,7 +10,7 @@ import { } from '../projects/projectsSlice'; import { toggleOpenLoupe } from '../loupe/loupeSlice'; import { setFocus, setSelectedImageIndices } from '../review/reviewSlice.js'; -import { fetchImages, setDeleteImagesAlertStatus } from '../images/imagesSlice.js'; +import { fetchImages, fetchImagesCount, setDeleteImagesAlertStatus } from '../images/imagesSlice.js'; const initialState = { loadingStates: { @@ -412,18 +412,18 @@ export const fetchTask = (taskId) => { }, DeleteImages: { COMPLETE: (res) => { - if (res.task._id === getState().tasks.loadingStates.deleteImages.taskId) { - console.log('deleteImages - res: ', res); - dispatch( - setFocus({ - index: { image: null, object: null, label: null }, - type: 'auto', - }), - ); - dispatch(setSelectedImageIndices([])); - dispatch(deleteImagesSuccess(res.task.output.imageIds)); - dispatch(setDeleteImagesAlertStatus({ openStatus: false })); - } + const filters = getState().filters.activeFilters; + dispatch( + setFocus({ + index: { image: null, object: null, label: null }, + type: 'auto', + }), + ); + dispatch(setSelectedImageIndices([])); + dispatch(deleteImagesSuccess(res.task.output.imageIds)); + dispatch(setDeleteImagesAlertStatus({ openStatus: false })); + dispatch(fetchImages(filters)); + dispatch(fetchImagesCount(filters)); }, FAIL: (res) => dispatch(deleteImagesFailure(res)) }, @@ -439,6 +439,7 @@ export const fetchTask = (taskId) => { dispatch(deleteImagesSuccess([])); dispatch(setDeleteImagesAlertStatus({ openStatus: false })); dispatch(fetchImages(res.task.output.filters)); + dispatch(fetchImagesCount(res.task.output.filters)); }, FAIL: (res) => dispatch(deleteImagesFailure(res))