diff --git a/src/features/images/ImagesTable.jsx b/src/features/images/ImagesTable.jsx
index 9b5c1fb1..f159a11d 100644
--- a/src/features/images/ImagesTable.jsx
+++ b/src/features/images/ImagesTable.jsx
@@ -4,6 +4,7 @@ import React, {
useEffect,
useRef,
useCallback,
+ forwardRef
} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { DateTime } from 'luxon';
@@ -26,10 +27,14 @@ import {
} from './imagesSlice';
import {
setFocus,
+ labelsAdded,
selectFocusIndex,
selectFocusChangeType
} from '../review/reviewSlice';
import { toggleOpenLoupe, selectLoupeOpen } from '../loupe/loupeSlice';
+import { selectUserUsername, selectUserCurrentRoles } from '../user/userSlice.js';
+import { selectAvailLabels } from '../filters/filtersSlice.js';
+import { selectIsAddingLabel, addLabelStart, addLabelEnd } from '../loupe/loupeSlice.js';
import { Image } from '../../components/Image';
import LabelPills from './LabelPills';
import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner';
@@ -42,6 +47,9 @@ import {
ContextMenuSeparator,
ContextMenuItemIconLeft
} from '../../components/ContextMenu';
+import CreatableSelect from 'react-select/creatable';
+import { createFilter } from 'react-select';
+
// TODO: make table horizontally scrollable on smaller screens
@@ -252,20 +260,17 @@ const ImagesTable = ({ workingImages, hasNext, loadNextPage }) => {
setSelectedRows([]);
}
}, [focusIndex.image]);
- console.log('selectedRows: ', selectedRows);
- const handleRowClick = useCallback((e, rowId) => {
+ const handleRowClick = useCallback((e, rowIdx) => {
if (e.shiftKey) {
- // TODO: allow for selection of mulitple images to perform bulk actions on
- console.log('shift + click detected. Current focusIndex: ', focusIndex);
- console.log('row clicked: ', Number(rowId));
- const start = Math.min(focusIndex.image, rowId);
- const end = Math.max(focusIndex.image, rowId);
+ // allow for selection of multiple images to perform bulk actions on
+ const start = Math.min(focusIndex.image, rowIdx);
+ const end = Math.max(focusIndex.image, rowIdx);
let selection = [];
for (let i = start; i <= end; i++) { selection.push(i); }
setSelectedRows(selection);
} else {
- const newIndex = { image: Number(rowId), object: null, label: null }
+ const newIndex = { image: Number(rowIdx), object: null, label: null }
dispatch(setFocus({ index: newIndex, type: 'manual' }));
dispatch(toggleOpenLoupe(true));
}
@@ -409,6 +414,23 @@ const ImagesTable = ({ workingImages, hasNext, loadNextPage }) => {
const row = rows[index];
prepareRow(row);
const selected = selectedRows.includes(index);
+ const selectedImages = selectedRows.map((rowIdx) => workingImages[rowIdx]);
+
+ // manage category selector state (open/closed)
+ const isAddingLabel = useSelector(selectIsAddingLabel);
+ const [ catSelectorOpen, setCatSelectorOpen ] = useState((isAddingLabel === 'from-image-table'));
+ useEffect(() => {
+ setCatSelectorOpen(((isAddingLabel === 'from-image-table')));
+ }, [isAddingLabel]);
+
+ const catSelectorRef = useRef(null);
+
+ const handleEditAllLabelsButtonClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ dispatch(addLabelStart('from-image-table'));
+ };
+
return (
@@ -467,6 +489,18 @@ const ImagesTable = ({ workingImages, hasNext, loadNextPage }) => {
Invalidate
+ {catSelectorOpen
+ ? ()
+ : (
+
+
+
+ Edit all labels
+ )
+ }
);
@@ -616,6 +650,112 @@ function makeRows(workingImages, focusIndex, selectedRows) {
...img,
}
})
-}
+};
+
+// TODO: make this it's own component.
+// Used in ImageReviewToolbar and BoundingBoxLabel
+
+const StyledCategorySelector = styled(CreatableSelect, {
+ width: '155px',
+ fontFamily: '$mono',
+ fontSize: '$2',
+ fontWeight: '$1',
+ zIndex: '$5',
+ '.react-select__control': {
+ boxSizing: 'border-box',
+ // height: '24px',
+ minHeight: 'unset',
+ border: '1px solid',
+ borderColor: '$border',
+ borderRadius: '$2',
+ cursor: 'pointer',
+ },
+ '.react-select__single-value': {
+ // position: 'relative',
+ },
+ '.react-select__indicator-separator': {
+ display: 'none',
+ },
+ '.react-select__dropdown-indicator': {
+ paddingTop: '0',
+ paddingBottom: '0',
+ },
+ '.react-select__control--is-focused': {
+ transition: 'all 0.2s ease',
+ boxShadow: '0 0 0 3px $blue200',
+ borderColor: '$blue500',
+ '&:hover': {
+ boxShadow: '0 0 0 3px $blue200',
+ borderColor: '$blue500',
+ },
+ },
+ '.react-select__menu': {
+ color: '$textDark',
+ fontSize: '$3',
+ '.react-select__option': {
+ cursor: 'pointer',
+ },
+ '.react-select__option--is-selected': {
+ color: '$blue500',
+ backgroundColor: '$blue200',
+ },
+ '.react-select__option--is-focused': {
+ backgroundColor: '$gray3',
+ },
+ }
+});
+
+const CategorySelector = ({ selectedImages }) => {
+ const userId = useSelector(selectUserUsername);
+ const dispatch = useDispatch();
+ // update selector options when new labels become available
+ const createOption = (category) => ({ value: category.toLowerCase(), label: category });
+ const availLabels = useSelector(selectAvailLabels);
+ const options = availLabels.ids.map((id) => createOption(id));
+
+ const handleCategoryChange = (newValue) => {
+ if (!newValue) return;
+ let labelsToAdd = [];
+ for (const image of selectedImages) {
+ const newLabels = image.objects
+ .filter((obj) => !obj.locked)
+ .map((obj) => ({
+ objIsTemp: obj.isTemp,
+ userId,
+ bbox: obj.bbox,
+ category: newValue.value || newValue,
+ objId: obj._id,
+ imgId: image._id
+ }));
+ labelsToAdd = labelsToAdd.concat(newLabels);
+ }
+ dispatch(labelsAdded({ labels: labelsToAdd }));
+ };
+
+ const handleCategorySelectorBlur = (e) => {
+ console.log('handleCategorySelectorBlur')
+ dispatch(addLabelEnd());
+ };
+
+ return (
+
+ );
+};
export default ImagesTable;
diff --git a/src/features/loupe/BoundingBox.jsx b/src/features/loupe/BoundingBox.jsx
index 5610d382..82d90d7b 100644
--- a/src/features/loupe/BoundingBox.jsx
+++ b/src/features/loupe/BoundingBox.jsx
@@ -125,6 +125,8 @@ const BoundingBox = ({
const username = useSelector(selectUserUsername);
const isAuthorized = hasRole(userRoles, WRITE_OBJECTS_ROLES);
const handleRef = useRef(null);
+ // TODO: I'm pretty sure we want to wrap the bounding box label in react.forwardRef
+ // https://react.dev/reference/react/forwardRef
const catSelectorRef = useRef(null);
const focusRef = useRef(null);
const dispatch = useDispatch();
@@ -248,7 +250,7 @@ const BoundingBox = ({
focusRef.current = catSelectorRef.current;
const newIndex = { image: focusIndex.image, object: objectIndex, label: null }
dispatch(setFocus({ index: newIndex, type: 'manual'}));
- dispatch(addLabelStart('to-single-object'));
+ dispatch(addLabelStart('from-object'));
};
const handleUnlockMenuItemClick = (e) => {
diff --git a/src/features/loupe/BoundingBoxLabel.jsx b/src/features/loupe/BoundingBoxLabel.jsx
index 421d3441..1c5705f2 100644
--- a/src/features/loupe/BoundingBoxLabel.jsx
+++ b/src/features/loupe/BoundingBoxLabel.jsx
@@ -145,10 +145,10 @@ const BoundingBoxLabel = ({
// manage category selector state (open/closed)
const isAddingLabel = useSelector(selectIsAddingLabel);
- const open = ((isAddingLabel === 'to-single-object') && selected);
+ const open = ((isAddingLabel === 'from-object') && selected);
const [ catSelectorOpen, setCatSelectorOpen ] = useState(open);
useEffect(() => {
- setCatSelectorOpen(((isAddingLabel === 'to-single-object') && selected));
+ setCatSelectorOpen(((isAddingLabel === 'from-object') && selected));
}, [isAddingLabel, selected]);
// manually focus catSelector if it's open
@@ -160,7 +160,7 @@ const BoundingBoxLabel = ({
e.stopPropagation();
if (!object.locked && isAuthorized && !catSelectorOpen) {
dispatch(setFocus({ index, type: 'manual' }));
- dispatch(addLabelStart('to-single-object'));
+ dispatch(addLabelStart('from-object'));
}
};
diff --git a/src/features/loupe/DrawBboxOverlay.jsx b/src/features/loupe/DrawBboxOverlay.jsx
index 5dd60c5e..d1a0d515 100644
--- a/src/features/loupe/DrawBboxOverlay.jsx
+++ b/src/features/loupe/DrawBboxOverlay.jsx
@@ -91,7 +91,7 @@ const DrawBboxOverlay = ({ imgContainerDims, imgDims, setTempObject }) => {
setTempBBox(defaultBBox);
dispatch(setFocus({ index: { object: null }, type: 'auto' }));
dispatch(drawBboxEnd());
- dispatch(addLabelStart('to-single-object'));
+ dispatch(addLabelStart('from-object'));
};
const startDrawingBBox = () => {
diff --git a/src/features/loupe/ImageReviewToolbar.jsx b/src/features/loupe/ImageReviewToolbar.jsx
index cc365e1b..3a7dd874 100644
--- a/src/features/loupe/ImageReviewToolbar.jsx
+++ b/src/features/loupe/ImageReviewToolbar.jsx
@@ -209,14 +209,14 @@ const ImageReviewToolbar = ({
// manage category selector state (open/closed)
const isAddingLabel = useSelector(selectIsAddingLabel);
- const [ catSelectorOpen, setCatSelectorOpen ] = useState((isAddingLabel === 'to-all-objects'));
+ const [ catSelectorOpen, setCatSelectorOpen ] = useState((isAddingLabel === 'from-review-toolbar'));
useEffect(() => {
- setCatSelectorOpen(((isAddingLabel === 'to-all-objects')));
+ setCatSelectorOpen(((isAddingLabel === 'from-review-toolbar')));
}, [isAddingLabel]);
const handleEditAllLabelsButtonClick = (e) => {
e.stopPropagation();
- dispatch(addLabelStart('to-all-objects'));
+ dispatch(addLabelStart('from-review-toolbar'));
};
const allObjectsLocked = image.objects && image.objects.every((obj) => obj.locked);
diff --git a/src/features/loupe/Loupe.jsx b/src/features/loupe/Loupe.jsx
index 88929e58..d44509b1 100644
--- a/src/features/loupe/Loupe.jsx
+++ b/src/features/loupe/Loupe.jsx
@@ -284,7 +284,7 @@ const Loupe = () => {
// ctrl-e (edit all)
if (((e.ctrlKey || e.metaKey) && charCode === 'e') &&
hasRole(userRoles, WRITE_OBJECTS_ROLES)) {
- dispatch(addLabelStart('to-all-objects'));
+ dispatch(addLabelStart('from-review-toolbar'));
}
// ctrl-v (repeat last action)
diff --git a/src/features/loupe/loupeSlice.js b/src/features/loupe/loupeSlice.js
index e887894c..4c424738 100644
--- a/src/features/loupe/loupeSlice.js
+++ b/src/features/loupe/loupeSlice.js
@@ -38,7 +38,7 @@ export const loupeSlice = createSlice({
},
addLabelStart: (state, { payload }) => {
- // payload can be 'to-single-object' or 'all-objects
+ // payload can be 'from-object' or 'all-objects
state.isAddingLabel = payload;
},