diff --git a/src/features/filters/LabelFilter.jsx b/src/features/filters/LabelFilter.jsx
index 0acb6e9b..f48c453b 100644
--- a/src/features/filters/LabelFilter.jsx
+++ b/src/features/filters/LabelFilter.jsx
@@ -1,8 +1,8 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { styled } from '../../theme/stitches.config.js';
import {
- selectAvailLabels,
+ selectAvailLabelFilters,
selectActiveFilters,
checkboxFilterToggled
} from './filtersSlice.js';
@@ -12,13 +12,12 @@ import Checkbox from '../../components/Checkbox.jsx';
import NoneFoundAlert from '../../components/NoneFoundAlert.jsx';
import { CheckboxLabel } from '../../components/CheckboxLabel.jsx';
import { CheckboxWrapper } from '../../components/CheckboxWrapper.jsx';
-import { selectLabelsLoading, checkboxOnlyButtonClicked } from './filtersSlice.js';
+import { checkboxOnlyButtonClicked } from './filtersSlice.js';
const LabelFilter = () => {
- const availLabels = useSelector(selectAvailLabels);
+ const availLabels = useSelector(selectAvailLabelFilters);
const activeFilters = useSelector(selectActiveFilters);
const activeLabels = activeFilters.labels;
- const labelsLoading = useSelector(selectLabelsLoading);
const dispatch = useDispatch();
const handleCheckboxChange = (e) => {
@@ -29,36 +28,40 @@ const LabelFilter = () => {
dispatch(checkboxFilterToggled(payload));
};
+ const managedIds = useMemo(() => availLabels.options.map(({ _id }) => _id), [availLabels.options]);
+ const sortedLabels = [...availLabels.options].sort((labelA, labelB) => labelA.name.toLowerCase() > labelB.name.toLowerCase() ? 1 : -1);
+
return (
- {labelsLoading.noneFound && no labels found }
- {availLabels.ids.length > 0 &&
+ {availLabels.options.length === 0 && no labels found }
+ {availLabels.options.length > 0 &&
<>
- {availLabels.ids.map((id) => {
- const checked = activeLabels === null || activeLabels.includes(id);
+ {sortedLabels.map(({ _id, name }) => {
+ const checked = activeLabels === null || activeLabels.includes(_id);
return (
-
+
@@ -87,6 +90,7 @@ const OnlyButton = styled('div', {
const LabelCheckboxLabel = ({
id,
+ name,
checked,
active,
filterCat,
@@ -106,7 +110,7 @@ const LabelCheckboxLabel = ({
onMouseEnter={() => setShowOnlyButton(true)}
onMouseLeave={() => setShowOnlyButton(false)}
>
- {id}
+ {name}
{showOnlyButton &&
only
}
diff --git a/src/features/filters/filtersSlice.js b/src/features/filters/filtersSlice.js
index 1065f359..d8b00ef7 100644
--- a/src/features/filters/filtersSlice.js
+++ b/src/features/filters/filtersSlice.js
@@ -5,10 +5,12 @@ import { registerCameraSuccess } from '../cameras/wirelessCamerasSlice';
import {
getProjectsStart,
getProjectsFailure,
- // registerCameraSuccess,
setSelectedProjAndView,
editDeploymentsSuccess,
+ createProjectLabelSuccess,
+ updateProjectLabelSuccess,
selectProjectsLoading,
+ selectProjectLabelsLoading,
} from '../projects/projectsSlice';
import {
normalizeFilters,
@@ -19,17 +21,9 @@ import {
const initialState = {
availFilters: {
- cameras: { ids: [] },
- deployments: { ids: [] },
- labels: {
- ids: [],
- loadingState: {
- isLoading: false,
- operation: null,
- errors: null,
- noneFound: false,
- },
- }
+ cameras: { options: [] },
+ deployments: { options: [] },
+ labels: { options: [] }
},
activeFilters: {
cameras: null,
@@ -50,38 +44,10 @@ export const filtersSlice = createSlice({
initialState,
reducers: {
- getLabelsStart: (state) => {
- state.availFilters.labels.loadingState.isLoading = true;
- state.availFilters.labels.loadingState.operation = 'fetching';
- },
-
- getLabelsFailure: (state, { payload }) => {
- let loadingState = state.availFilters.labels.loadingState;
- loadingState.isLoading = false;
- loadingState.operation = null;
- loadingState.errors = payload;
- state.availFilters.labels.ids = [];
- },
-
- getLabelsSuccess: (state, { payload }) => {
- let loadingState = state.availFilters.labels.loadingState;
- loadingState.isLoading = false;
- loadingState.operation = null;
- loadingState.errors = null;
- payload.labels.categories.forEach((cat) => {
- if (!state.availFilters.labels.ids.includes(cat)) {
- state.availFilters.labels.ids.push(cat);
- }
- });
- if (payload.labels.categories.length === 0) {
- loadingState.noneFound = true;
- }
- },
-
checkboxFilterToggled: (state, { payload }) => {
const { filterCat, val } = payload;
const activeIds = state.activeFilters[filterCat];
- const availIds = state.availFilters[filterCat].ids;
+ const availIds = state.availFilters[filterCat].options.map(({ _id }) => _id);
if (activeIds === null) {
// if null, all filters are selected, so toggling one = unselecting it
@@ -130,7 +96,7 @@ export const filtersSlice = createSlice({
bulkSelectToggled: (state, { payload }) => {
const { currState, filterCat, managedIds } = payload;
const activeIds = state.activeFilters[filterCat];
- const availIds = state.availFilters[filterCat].ids;
+ const availIds = state.availFilters[filterCat].options.map(({ _id }) => _id);
let newActiveIds;
if (currState === 'noneSelected') {
@@ -164,22 +130,10 @@ export const filtersSlice = createSlice({
[filterCat]
);
},
-
},
extraReducers: (builder) => {
builder
- .addCase(getProjectsStart, (state, { payload }) => {
- let loadingState = state.availFilters.labels.loadingState;
- loadingState.isLoading = true;
- loadingState.operation = 'fetching';
- })
- .addCase(getProjectsFailure, (state, { payload }) => {
- let loadingState = state.availFilters.labels.loadingState;
- loadingState.isLoading = false;
- loadingState.operation = null;
- loadingState.errors = payload;
- })
.addCase(setSelectedProjAndView, (state, { payload }) => {
const { cameraConfigs, labels } = payload.project;
updateAvailDepFilters(state, cameraConfigs);
@@ -188,6 +142,20 @@ export const filtersSlice = createSlice({
// set all filters to new selected view? We're currently handling this
// by dispatching setActiveFilters from setSelectedProjAndViewMiddleware
})
+ .addCase(createProjectLabelSuccess, (state, { payload }) => {
+ const labels = [...state.availFilters.labels.options, payload.label];
+ updateAvailLabelFilters(state, labels);
+ })
+ .addCase(updateProjectLabelSuccess, (state, { payload }) => {
+ const labels = state.availFilters.labels.options.map((label) => {
+ if (label._id === payload.label._id) {
+ return payload.label;
+ } else {
+ return label;
+ }
+ });
+ updateAvailLabelFilters(state, labels);
+ })
.addCase(registerCameraSuccess, (state, { payload }) => {
const { cameraConfigs } = payload.project;
updateAvailDepFilters(state, cameraConfigs);
@@ -195,7 +163,7 @@ export const filtersSlice = createSlice({
})
.addCase(editDeploymentsSuccess, (state, { payload }) => {
const { operation, reqPayload } = payload;
- const availDepFilters = state.availFilters.deployments.ids;
+ const availDepFilters = state.availFilters.deployments.options.map(({ _id }) => _id);
const activeDepFilters = state.activeFilters.deployments;
switch (operation) {
case 'updateDeployment': { break }
@@ -203,10 +171,10 @@ export const filtersSlice = createSlice({
// add new dep to available deployment filters
const newDepId = reqPayload.deployment._id;
if (!availDepFilters) {
- state.availFilters.deployments.ids = [newDepId];
+ state.availFilters.deployments.options = [{ _id: newDepId }];
}
else if (!availDepFilters.includes(newDepId)) {
- state.availFilters.deployments.ids.push(newDepId);
+ state.availFilters.deployments.options.push({ _id: newDepId });
}
// and active deployment filters
if (activeDepFilters &&
@@ -217,9 +185,11 @@ export const filtersSlice = createSlice({
}
case 'deleteDeployment': {
// remove deleted dep from available and active deployment filters
- state.availFilters.deployments.ids = availDepFilters.filter((id) => (
- id !== reqPayload.deploymentId
+ const filteredDeps = state.availFilters.deployments.options.filter((opt) => (
+ opt._id !== reqPayload.deploymentId
));
+ state.availFilters.deployments.options = filteredDeps;
+
state.activeFilters.deployments = (activeDepFilters !== null)
? activeDepFilters.filter((id) => id !== reqPayload.deploymentId)
: null;
@@ -235,9 +205,6 @@ export const filtersSlice = createSlice({
});
export const {
- getLabelsStart,
- getLabelsSuccess,
- getLabelsFailure,
getModelsSuccess,
checkboxFilterToggled,
reviewedFilterToggled,
@@ -249,36 +216,12 @@ export const {
checkboxOnlyButtonClicked,
} = filtersSlice.actions;
-// TODO: maybe use createAsyncThunk for these?
-// https://redux-toolkit.js.org/api/createAsyncThunk
-
-// fetchLabels thunk
-export const fetchLabels = () => async (dispatch, getState)=> {
- try {
- dispatch(getLabelsStart());
- const currentUser = await Auth.currentAuthenticatedUser();
- const token = currentUser.getSignInUserSession().getIdToken().getJwtToken();
- const projects = getState().projects.projects
- const selectedProj = projects.find((proj) => proj.selected);
- if (token) {
- const labels = await call({
- projId: selectedProj._id,
- request: 'getLabels',
- });
- dispatch(getLabelsSuccess(labels));
- }
- } catch (err) {
- dispatch(getLabelsFailure(err));
- }
-};
-
// Selectors
export const selectActiveFilters = state => state.filters.activeFilters;
export const selectAvailFilters = state => state.filters.availFilters;
-export const selectAvailCameras = state => state.filters.availFilters.cameras;
-export const selectAvailDeployments = state => state.filters.availFilters.deployments;
-export const selectAvailLabels = state => state.filters.availFilters.labels;
-export const selectLabelsLoading = state => state.filters.availFilters.labels.loadingState;
+export const selectAvailCameraFilters = state => state.filters.availFilters.cameras;
+export const selectAvailDeploymentFilters = state => state.filters.availFilters.deployments;
+export const selectAvailLabelFilters = state => state.filters.availFilters.labels;
export const selectReviewed = state => state.filters.activeFilters.reviewed;
export const selectNotReviewed = state => state.filters.activeFilters.notReviewed;
export const selectCustomFilter = state => state.filters.activeFilters.custom;
@@ -290,12 +233,5 @@ export const selectDateCreatedFilter = state => ({
start: state.filters.activeFilters.createdStart,
end: state.filters.activeFilters.createdEnd,
});
-export const selectFiltersReady = createSelector(
- [selectProjectsLoading, selectLabelsLoading],
- (projectsLoading, labelsLoading) => {
- const dependencies = [projectsLoading, labelsLoading];
- return !dependencies.some(d => d.isLoading || d.errors);
- }
-);
export default filtersSlice.reducer;
diff --git a/src/features/filters/utils.js b/src/features/filters/utils.js
index 7accb9d4..3b8d3ef3 100644
--- a/src/features/filters/utils.js
+++ b/src/features/filters/utils.js
@@ -5,7 +5,7 @@ const normalizeFilters = (
) => {
// if all available ids are selected for a filter category, set to null
for (const filtCat of filterCats) {
- const availIds = availFilts[filtCat].ids;
+ const availIds = availFilts[filtCat].options.map(({ _id }) => _id);
const activeIds = newActiveFilts[filtCat];
if ((availIds && activeIds) && (availIds.length === activeIds.length)) {
newActiveFilts[filtCat] = null;
@@ -17,27 +17,30 @@ const normalizeFilters = (
const updateAvailDepFilters = (state, camConfigs) => {
const newDeps = camConfigs.reduce((acc, camConfig) => {
for (const dep of camConfig.deployments) {
- acc.push(dep._id);
+ acc.push({ _id: dep._id });
}
return acc;
},[]);
- state.availFilters.deployments.ids = newDeps;
+ state.availFilters.deployments.options = newDeps;
}
const updateAvailCamFilters = (state, camConfigs) => {
- state.availFilters.cameras.ids = camConfigs.map((cc) => cc._id);
+ state.availFilters.cameras.options = camConfigs.map((cc) => ({ _id: cc._id }));
};
const updateAvailLabelFilters = (state, labels) => {
- state.availFilters.labels.ids = labels.categories;
- const noneFound = (labels.categories.length === 0);
- state.availFilters.labels.loadingState = {
- isLoading: false,
- operation: null,
- errors: null,
- noneFound,
- };
-}
+ const defaultLabelFilters = [
+ {
+ _id: 'none',
+ name: 'none',
+ color: '#AFE790',
+ }
+ ];
+ const defaults = defaultLabelFilters.filter((defaultLabel) => (
+ !labels.find((lbl) => lbl._id.toString() === defaultLabel._id.toString())
+ ));
+ state.availFilters.labels.options = [...defaults, ...labels];
+};
export {
normalizeFilters,
diff --git a/src/features/images/ImagesTableRow.jsx b/src/features/images/ImagesTableRow.jsx
index 0ee9b539..19cb6be1 100644
--- a/src/features/images/ImagesTableRow.jsx
+++ b/src/features/images/ImagesTableRow.jsx
@@ -159,7 +159,7 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices })
objIsTemp: obj.isTemp,
userId,
bbox: obj.bbox,
- category: newValue.value || newValue,
+ labelId: newValue.value || newValue,
objId: obj._id,
imgId: image._id
}));
@@ -246,7 +246,7 @@ const ImagesTableRow = ({ row, index, focusIndex, style, selectedImageIndices })
let existingEmptyLabels = [];
image.objects.forEach((obj) => {
obj.labels
- .filter((lbl) => lbl.category === 'empty' && !lbl.validated)
+ .filter((lbl) => lbl.labelId === 'empty' && !lbl.validated)
.forEach((lbl) => {
existingEmptyLabels.push({
imgId: image._id,
diff --git a/src/features/images/LabelPills.jsx b/src/features/images/LabelPills.jsx
index f01795af..74e35aa2 100644
--- a/src/features/images/LabelPills.jsx
+++ b/src/features/images/LabelPills.jsx
@@ -1,32 +1,10 @@
import React from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { styled, labelColors } from '../../theme/stitches.config.js';
+import { selectLabels } from '../projects/projectsSlice.js';
import { setFocus } from '../review/reviewSlice.js';
import { toggleOpenLoupe } from '../loupe/loupeSlice.js';
-
-
-const LabelPill = styled('div', {
- color: '$textDark',
- fontSize: '$2',
- fontWeight: '$5',
- fontFamily: '$mono',
- padding: '$1 $3',
- '&:not(:last-child)': {
- marginRight: '$2',
- },
- borderRadius: '$3',
- border: '1px solid rgba(0,0,0,0)',
- transition: 'all 0.2s ease',
- variants: {
- focused: {
- true: {
- outline: 'none',
- boxShadow: '0 0 0 3px $blue200',
- borderColor: '$blue500',
- }
- }
- }
-});
+import LabelPill from '../../components/LabelPill.jsx';
const ObjectPill = styled('div', {
display: 'flex',
@@ -66,6 +44,7 @@ const LabelContainer = styled('div', {
const LabelPills = ({ objects, imageIndex, focusIndex }) => {
const isImageFocused = imageIndex === focusIndex.image;
const dispatch = useDispatch();
+ const projectLabels = useSelector(selectLabels);
const handleLabelPillClick = (e, objIndex, lblIndex) => {
// if user isn't attempting a multi-row selection, update focus
@@ -79,7 +58,6 @@ const LabelPills = ({ objects, imageIndex, focusIndex }) => {
return (
{objects.map((object, objIndex) => {
-
// TODO: find a cleaner way to do this. Maybe make it a hook?
// We also need filtered objects in FullSizeImage component...
// and reviewMiddleware so consider encapsulating
@@ -107,6 +85,7 @@ const LabelPills = ({ objects, imageIndex, focusIndex }) => {
>
{labels.map((label, i) => {
const lblIndex = object.labels.indexOf(label);
+ const l = projectLabels?.find(({ _id }) => label.labelId === _id);
return (
{
lblIndex === focusIndex.label
}
onClick={(e) => handleLabelPillClick(e, objIndex, lblIndex)}
- css={{
- backgroundColor: labelColors(label.category).bg,
- borderColor: labelColors(label.category).border,
- color: labelColors(label.category).textDark,
- }}
- >
- {label.category}
-
+ color={l?.color || '#00C797'}
+ name={l?.name || "ERROR FINDING LABEL"}
+ />
)
})}
@@ -135,4 +109,4 @@ const LabelPills = ({ objects, imageIndex, focusIndex }) => {
)
};
-export default LabelPills;
\ No newline at end of file
+export default LabelPills;
diff --git a/src/features/loupe/BoundingBox.jsx b/src/features/loupe/BoundingBox.jsx
index bcecbb82..fa2b6934 100644
--- a/src/features/loupe/BoundingBox.jsx
+++ b/src/features/loupe/BoundingBox.jsx
@@ -25,6 +25,7 @@ import { addLabelStart } from './loupeSlice';
import BoundingBoxLabel from './BoundingBoxLabel';
import { absToRel, relToAbs } from '../../app/utils';
import { CheckIcon, Cross2Icon, LockOpen1Icon, Pencil1Icon } from '@radix-ui/react-icons';
+import { selectLabels } from '../projects/projectsSlice';
const ResizeHandle = styled('div', {
width: '$3',
@@ -87,7 +88,7 @@ const DragHandle = styled('div', {
const StyledResizableBox = styled(ResizableBox, {
boxSizing: 'border-box',
position: 'absolute !important;',
- border: '2px solid #345EFF',
+ border: '2px solid #00C797',
// zIndex: '$2',
variants: {
selected: {
@@ -147,6 +148,9 @@ const BoundingBox = ({
label = object.labels[focusIndex.label];
}
+ const projectLabels = useSelector(selectLabels);
+ const displayLabel = projectLabels?.find(({ _id }) => _id === label.labelId);
+
// set label color and confidence
// TODO: maybe this belongs in label component?
const conf = Number.parseFloat(label.conf * 100).toFixed(1);
@@ -289,8 +293,8 @@ const BoundingBox = ({
selected={objectFocused}
locked={object.locked}
css={{
- borderColor: labelColor.base,
- background: labelColor.base + '0D',
+ borderColor: displayLabel?.color,
+ background: displayLabel?.color + '0D',
}}
>
{label &&
@@ -300,6 +304,7 @@ const BoundingBox = ({
object={object}
label={label}
labelColor={labelColor}
+ displayLabel={displayLabel}
conf={conf}
selected={objectFocused}
showLabelButtons={showLabelButtons}
@@ -388,4 +393,4 @@ const BoundingBox = ({
)
};
-export default BoundingBox;
\ No newline at end of file
+export default BoundingBox;
diff --git a/src/features/loupe/BoundingBoxLabel.jsx b/src/features/loupe/BoundingBoxLabel.jsx
index f2aa2aa6..5c337734 100644
--- a/src/features/loupe/BoundingBoxLabel.jsx
+++ b/src/features/loupe/BoundingBoxLabel.jsx
@@ -1,11 +1,11 @@
import React, { useState, useEffect, forwardRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { styled } from '../../theme/stitches.config.js';
-import { selectAvailLabels } from '../filters/filtersSlice.js';
import { labelsAdded, setFocus } from '../review/reviewSlice.js';
import { addLabelStart, addLabelEnd, selectIsAddingLabel } from './loupeSlice.js';
import ValidationButtons from './ValidationButtons.jsx';
import CategorySelector from '../../components/CategorySelector.jsx';
+import { getTextColor } from '../../app/utils.js';
const StyledBoundingBoxLabel = styled('div', {
// backgroundColor: '#345EFF',
@@ -71,6 +71,7 @@ const BoundingBoxLabel = forwardRef(({
object,
label,
labelColor,
+ displayLabel,
conf,
selected,
showLabelButtons,
@@ -112,7 +113,7 @@ const BoundingBoxLabel = forwardRef(({
objIsTemp: object.isTemp,
userId: username,
bbox: object.bbox,
- category: newValue.value || newValue,
+ labelId: newValue.value,
objId: object._id,
imgId
}]
@@ -131,7 +132,7 @@ const BoundingBoxLabel = forwardRef(({
catSelectorOpen={catSelectorOpen}
selected={selected}
css={{
- backgroundColor: labelColor.base,
+ backgroundColor: displayLabel?.color,
color: textColor, // labelColor.bg
}}
>
@@ -143,8 +144,8 @@ const BoundingBoxLabel = forwardRef(({
handleCategorySelectorBlur={handleCategorySelectorBlur}
menuPlacement='bottom'
/>
-
- {label.category}
+
+ {displayLabel?.name || "ERROR FINDING LABEL"}
{!object.locked && {conf}% }
@@ -153,7 +154,7 @@ const BoundingBoxLabel = forwardRef(({
imgId={imgId}
object={object}
label={label}
- labelColor={labelColor}
+ labelColor={displayLabel?.color}
username={username}
/>
}
diff --git a/src/features/loupe/DrawBboxOverlay.jsx b/src/features/loupe/DrawBboxOverlay.jsx
index d1a0d515..a6f93e7c 100644
--- a/src/features/loupe/DrawBboxOverlay.jsx
+++ b/src/features/loupe/DrawBboxOverlay.jsx
@@ -162,4 +162,4 @@ const DrawBboxOverlay = ({ imgContainerDims, imgDims, setTempObject }) => {
);
};
-export default DrawBboxOverlay;
\ No newline at end of file
+export default DrawBboxOverlay;
diff --git a/src/features/loupe/ImageReviewToolbar.jsx b/src/features/loupe/ImageReviewToolbar.jsx
index a7e92232..41bc4cca 100644
--- a/src/features/loupe/ImageReviewToolbar.jsx
+++ b/src/features/loupe/ImageReviewToolbar.jsx
@@ -1,8 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { styled } from '../../theme/stitches.config.js';
-import CreatableSelect from 'react-select/creatable';
-import { createFilter } from 'react-select';
import {
Pencil1Icon,
GroupIcon,
@@ -14,7 +12,6 @@ import {
ChevronLeftIcon,
ChevronRightIcon
} from '@radix-ui/react-icons';
-import { selectAvailLabels } from '../filters/filtersSlice.js';
import IconButton from '../../components/IconButton.jsx';
import { labelsAdded } from '../review/reviewSlice.js';
import { addLabelStart, addLabelEnd, selectIsDrawingBbox, selectIsAddingLabel } from './loupeSlice.js';
@@ -123,7 +120,7 @@ const ImageReviewToolbar = ({
objIsTemp: obj.isTemp,
userId,
bbox: obj.bbox,
- category: newValue.value || newValue,
+ labelId: newValue.value || newValue,
objId: obj._id,
imgId: image._id
}));
diff --git a/src/features/loupe/Loupe.jsx b/src/features/loupe/Loupe.jsx
index 035a9bf7..7cb47313 100644
--- a/src/features/loupe/Loupe.jsx
+++ b/src/features/loupe/Loupe.jsx
@@ -171,7 +171,7 @@ const Loupe = () => {
objIsTemp: obj.isTemp,
userId,
bbox: obj.bbox,
- category: lastCategoryApplied,
+ labelId: lastCategoryApplied,
objId: obj._id,
imgId: image._id
}));
@@ -190,7 +190,7 @@ const Loupe = () => {
const labelsToValidate = [];
image.objects.forEach((obj) => {
obj.labels
- .filter((lbl) => lbl.category === 'empty' && !lbl.validated)
+ .filter((lbl) => lbl.labelId === 'empty' && !lbl.validated)
.forEach((lbl) => {
labelsToValidate.push({
imgId: image._id,
diff --git a/src/features/loupe/ValidationButtons.jsx b/src/features/loupe/ValidationButtons.jsx
index 2aae0222..b85737d1 100644
--- a/src/features/loupe/ValidationButtons.jsx
+++ b/src/features/loupe/ValidationButtons.jsx
@@ -59,8 +59,8 @@ const ValidationButtons = ({
alignItems: 'center',
justifyContent: 'center',
color: '$loContrast',
- backgroundColor: labelColor.base,
- borderColor: labelColor.base,
+ backgroundColor: labelColor,
+ borderColor: labelColor,
'&:hover': {
backgroundColor: '$loContrast',
color: '$hiContrast',
@@ -77,11 +77,11 @@ const ValidationButtons = ({
justifyContent: 'center',
backgroundColor: '#E04040',
color: '$loContrast',
- borderColor: labelColor.base,
+ borderColor: labelColor,
'&:hover': {
color: '#E04040',
backgroundColor: '$loContrast',
- borderColor: labelColor.base,
+ borderColor: labelColor,
}
}}
>
@@ -95,11 +95,11 @@ const ValidationButtons = ({
justifyContent: 'center',
backgroundColor: '#00C797',
color: '$loContrast',
- borderColor: labelColor.base,
+ borderColor: labelColor,
'&:hover': {
color: '#00C797',
backgroundColor: '$loContrast',
- borderColor: labelColor.base,
+ borderColor: labelColor,
}
}}
>
diff --git a/src/features/projects/AutomationRulesList.jsx b/src/features/projects/AutomationRulesList.jsx
index 9c88b689..16e4c5b7 100644
--- a/src/features/projects/AutomationRulesList.jsx
+++ b/src/features/projects/AutomationRulesList.jsx
@@ -36,6 +36,10 @@ const StyledRuleDescription = styled('div', {
}
});
+const EditButtons = styled('div', {
+ minWidth: '64px',
+});
+
const RuleDescription = ({ rule, availableModels }) => {
const model = availableModels.find((m) => m === rule.action.mlModel);
return (
@@ -90,7 +94,7 @@ const AutomationRulesList = ({ project, availableModels, onAddRuleClick, onEditR
return (
+ Disabling a label will prevent users from applying it to images going
+ forward, but it will not remove existing instances of the label on your images.
+
+);
+
+export default LabelForm;
diff --git a/src/features/projects/ManageLabelsModal/NewLabelForm.jsx b/src/features/projects/ManageLabelsModal/NewLabelForm.jsx
new file mode 100644
index 00000000..3c6b01ea
--- /dev/null
+++ b/src/features/projects/ManageLabelsModal/NewLabelForm.jsx
@@ -0,0 +1,83 @@
+import { useCallback, useMemo, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { Formik } from 'formik';
+import * as Yup from 'yup';
+
+import { createProjectLabel } from "../projectsSlice.js";
+import LabelPill from "../../../components/LabelPill";
+import Button from "../../../components/Button";
+import LabelForm from './LabelForm';
+import {
+ LabelRow,
+ LabelHeader,
+ LabelActions,
+} from './components';
+import { getRandomColor } from "../../../app/utils.js";
+
+const NewLabelForm = ({ labels }) => {
+ const dispatch = useDispatch();
+ const [ showNewLabelForm, setShowNewLabelForm ] = useState(false);
+
+ const toggleOpenForm = useCallback(() => setShowNewLabelForm((prev) => !prev), []);
+ const onSubmit = useCallback((values, { resetForm }) => {
+ dispatch(createProjectLabel(values));
+ setShowNewLabelForm(false);
+ resetForm();
+ });
+
+ const labelsNames = labels.map(({ name }) => name.toLowerCase());
+ const schema = useMemo(() => {
+ return Yup.object().shape({
+ name: Yup.string()
+ .required('Enter a label name.')
+ .matches(/^[a-zA-Z0-9_. -']*$/, 'Labels can\'t contain special characters')
+ .test(
+ 'unique',
+ 'A label with this name already exists.',
+ (val) => !labelsNames.includes(val?.toLowerCase())),
+ color: Yup.string()
+ .matches(/^#[0-9A-Fa-f]{6}$/, { message: "Enter a valid color code with 6 digits" })
+ .required('Select a color.'),
+ });
+ }, []);
+
+ return (
+