Skip to content

Commit

Permalink
Wire up edit-all-labels context menu item
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanielrindlaub committed Dec 6, 2023
1 parent e4a45a3 commit b4fb035
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 19 deletions.
158 changes: 149 additions & 9 deletions src/features/images/ImagesTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useEffect,
useRef,
useCallback,
forwardRef
} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { DateTime } from 'luxon';
Expand All @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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 (
<ContextMenu>
<ContextMenuTrigger disabled={!selected}>
Expand Down Expand Up @@ -467,6 +489,18 @@ const ImagesTable = ({ workingImages, hasNext, loadNextPage }) => {
</ContextMenuItemIconLeft>
Invalidate
</ContextMenuItem>
{catSelectorOpen
? (<CategorySelector selectedImages={selectedImages} />)
: (<ContextMenuItem
onSelect={handleEditAllLabelsButtonClick}
disabled={false}
>
<ContextMenuItemIconLeft>
<Pencil1Icon />
</ContextMenuItemIconLeft>
Edit all labels
</ContextMenuItem>)
}
</ContextMenuContent>
</ContextMenu>
);
Expand Down Expand Up @@ -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 (
<StyledCategorySelector
autoFocus
isClearable
isSearchable
openMenuOnClick
className='react-select'
classNamePrefix='react-select'
menuPlacement='bottom'
filterOption={createFilter({ matchFrom: 'start' })} // TODO: what does this do?
isLoading={availLabels.isLoading}
isDisabled={availLabels.isLoading}
onChange={handleCategoryChange}
onCreateOption={handleCategoryChange}
onBlur={handleCategorySelectorBlur}
// value={createOption(label.category)}
options={options}
/>
);
};

export default ImagesTable;
4 changes: 3 additions & 1 deletion src/features/loupe/BoundingBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) => {
Expand Down
6 changes: 3 additions & 3 deletions src/features/loupe/BoundingBoxLabel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'));
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/features/loupe/DrawBboxOverlay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
6 changes: 3 additions & 3 deletions src/features/loupe/ImageReviewToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/features/loupe/Loupe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/features/loupe/loupeSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},

Expand Down

0 comments on commit b4fb035

Please sign in to comment.