From aad5a6a1a93114823c2880e8808af97114a076a2 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Wed, 19 Mar 2025 10:03:33 +0100 Subject: [PATCH] Feat/tag type colors frontend (#9566) Add frontend support for tag type colors --- .../tags/CreateTagType/CreateTagType.tsx | 8 +- .../tags/EditTagType/EditTagType.tsx | 10 +- .../tags/TagTypeForm/TagTypeColorPicker.tsx | 103 +++++++++ .../tags/TagTypeForm/TagTypeForm.tsx | 25 ++- .../tags/TagTypeForm/useTagTypeForm.ts | 30 ++- .../tags/TagTypeList/TagTypeList.tsx | 44 +++- .../__snapshots__/TagTypeList.test.tsx.snap | 210 ++++++++++-------- frontend/src/interfaces/tags.ts | 2 + frontend/src/interfaces/uiConfig.ts | 1 + src/lib/types/experimental.ts | 7 +- src/server-dev.ts | 1 + 11 files changed, 329 insertions(+), 112 deletions(-) create mode 100644 frontend/src/component/tags/TagTypeForm/TagTypeColorPicker.tsx diff --git a/frontend/src/component/tags/CreateTagType/CreateTagType.tsx b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx index f2f505d7e9..7c608eb056 100644 --- a/frontend/src/component/tags/CreateTagType/CreateTagType.tsx +++ b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx @@ -17,12 +17,15 @@ const CreateTagType = () => { const { tagName, tagDesc, + color, setTagName, setTagDesc, + setColor, getTagPayload, validateNameUniqueness, errors, clearErrors, + isTagTypeColorEnabled, } = useTagTypeForm(); const { createTag, loading } = useTagTypesApi(); @@ -70,12 +73,15 @@ const CreateTagType = () => { handleSubmit={handleSubmit} handleCancel={handleCancel} tagName={tagName} - setTagName={setTagName} tagDesc={tagDesc} + color={color} + setTagName={setTagName} setTagDesc={setTagDesc} + setColor={setColor} mode='Create' clearErrors={clearErrors} validateNameUniqueness={validateNameUniqueness} + isTagTypeColorEnabled={isTagTypeColorEnabled} > diff --git a/frontend/src/component/tags/EditTagType/EditTagType.tsx b/frontend/src/component/tags/EditTagType/EditTagType.tsx index 4af677dffd..c4eb363a40 100644 --- a/frontend/src/component/tags/EditTagType/EditTagType.tsx +++ b/frontend/src/component/tags/EditTagType/EditTagType.tsx @@ -21,12 +21,15 @@ const EditTagType = () => { const { tagName, tagDesc, + color, setTagName, setTagDesc, + setColor, getTagPayload, errors, clearErrors, - } = useTagTypeForm(tagType?.name, tagType?.description); + isTagTypeColorEnabled, + } = useTagTypeForm(tagType?.name, tagType?.description, tagType?.color); const { updateTagType, loading } = useTagTypesApi(); const handleSubmit = async (e: Event) => { @@ -72,11 +75,14 @@ const EditTagType = () => { handleSubmit={handleSubmit} handleCancel={handleCancel} tagName={tagName} - setTagName={setTagName} tagDesc={tagDesc} + color={color} + setTagName={setTagName} setTagDesc={setTagDesc} + setColor={setColor} mode='Edit' clearErrors={clearErrors} + isTagTypeColorEnabled={isTagTypeColorEnabled} > diff --git a/frontend/src/component/tags/TagTypeForm/TagTypeColorPicker.tsx b/frontend/src/component/tags/TagTypeForm/TagTypeColorPicker.tsx new file mode 100644 index 0000000000..277a6594bd --- /dev/null +++ b/frontend/src/component/tags/TagTypeForm/TagTypeColorPicker.tsx @@ -0,0 +1,103 @@ +import type { FC } from 'react'; +import { styled, Box, useTheme } from '@mui/material'; + +interface ITagTypeColorPickerProps { + selectedColor: string; + onChange: (color: string) => void; +} + +interface IColorOption { + name: string; + value: string; +} + +const StyledColorContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + marginBottom: theme.spacing(2), + marginTop: theme.spacing(1), +})); + +const StyledColorsWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + marginTop: theme.spacing(1), +})); + +const StyledColorCircle = styled('button')<{ + $color: string; + $selected: boolean; +}>(({ theme, $color, $selected }) => ({ + width: '24px', + height: '24px', + borderRadius: '50%', + border: $selected + ? `2px solid ${theme.palette.primary.main}` + : $color === '#FFFFFF' + ? `1px solid ${theme.palette.divider}` + : `1px solid ${$color}`, + backgroundColor: $color, + cursor: 'pointer', + padding: 0, + '&:hover': { + boxShadow: theme.boxShadows.elevated, + }, +})); + +export const TagTypeColorPicker: FC = ({ + selectedColor, + onChange, +}) => { + const theme = useTheme(); + + const getColorWithFallback = (color: string | undefined): string => + color || '#FFFFFF'; + + const colorOptions: IColorOption[] = [ + { name: 'White', value: theme.palette.common.white }, + { + name: 'Green', + value: getColorWithFallback(theme.palette.success.border), + }, + { + name: 'Yellow', + value: getColorWithFallback(theme.palette.warning.border), + }, + { name: 'Red', value: theme.palette.error.main }, + { + name: 'Blue', + value: getColorWithFallback(theme.palette.info.border), + }, + { + name: 'Purple', + value: getColorWithFallback(theme.palette.secondary.border), + }, + { + name: 'Gray', + value: getColorWithFallback(theme.palette.neutral.border), + }, + ]; + + return ( + + + {colorOptions.map((color) => ( + + onChange(color.value)} + type='button' + aria-label={`Select ${color.name} color`} + /> + + ))} + + + ); +}; diff --git a/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx b/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx index 248874102c..65d73955d0 100644 --- a/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx +++ b/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx @@ -1,21 +1,25 @@ import Input from 'component/common/Input/Input'; -import { TextField, Button, styled } from '@mui/material'; - +import { TextField, Button, styled, Typography } from '@mui/material'; +import { TagTypeColorPicker } from './TagTypeColorPicker'; import type React from 'react'; import { trim } from 'component/common/util'; import { EDIT } from 'constants/misc'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; interface ITagTypeForm { tagName: string; tagDesc: string; + color: string; setTagName: React.Dispatch>; setTagDesc: React.Dispatch>; + setColor: React.Dispatch>; handleSubmit: (e: any) => void; handleCancel: () => void; errors: { [key: string]: string }; mode: 'Create' | 'Edit'; clearErrors: () => void; validateNameUniqueness?: () => void; + isTagTypeColorEnabled: boolean; children?: React.ReactNode; } @@ -59,12 +63,15 @@ const TagTypeForm: React.FC = ({ handleCancel, tagName, tagDesc, + color, setTagName, setTagDesc, + setColor, errors, mode, validateNameUniqueness, clearErrors, + isTagTypeColorEnabled, }) => { return ( @@ -95,6 +102,20 @@ const TagTypeForm: React.FC = ({ value={tagDesc} onChange={(e) => setTagDesc(e.target.value)} /> + + + Tag color + + + + } + /> {children} diff --git a/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts index fe4379b717..482ffeb006 100644 --- a/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts +++ b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts @@ -1,12 +1,25 @@ import { useEffect, useState } from 'react'; import useTagTypesApi from 'hooks/api/actions/useTagTypesApi/useTagTypesApi'; import { formatUnknownError } from 'utils/formatUnknownError'; +import { useUiFlag } from 'hooks/useUiFlag'; -const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => { +interface TagTypePayload { + name: string; + description: string; + color?: string; +} + +const useTagTypeForm = ( + initialTagName = '', + initialTagDesc = '', + initialColor = '#FFFFFF', +) => { const [tagName, setTagName] = useState(initialTagName); const [tagDesc, setTagDesc] = useState(initialTagDesc); + const [color, setColor] = useState(initialColor); const [errors, setErrors] = useState({}); const { validateTagName } = useTagTypesApi(); + const isTagTypeColorEnabled = Boolean(useUiFlag('tagTypeColor')); useEffect(() => { setTagName(initialTagName); @@ -16,11 +29,21 @@ const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => { setTagDesc(initialTagDesc); }, [initialTagDesc]); + useEffect(() => { + setColor(initialColor); + }, [initialColor]); + const getTagPayload = () => { - return { + const payload: TagTypePayload = { name: tagName, description: tagDesc, }; + + if (isTagTypeColorEnabled && color) { + payload.color = color; + } + + return payload; }; const validateNameUniqueness = async () => { @@ -54,12 +77,15 @@ const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => { return { tagName, tagDesc, + color, setTagName, setTagDesc, + setColor, getTagPayload, clearErrors, validateNameUniqueness, errors, + isTagTypeColorEnabled, }; }; diff --git a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx index a4ce5f25dc..dfc9bfcb27 100644 --- a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx +++ b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box } from '@mui/material'; +import { Box, styled } from '@mui/material'; import { Table, SortableTableHeader, @@ -31,6 +31,22 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { sortTypes } from 'utils/sortTypes'; import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton'; import { Search } from 'component/common/Search/Search'; +import { useUiFlag } from 'hooks/useUiFlag'; + +const StyledColorDot = styled('div')<{ $color: string }>( + ({ theme, $color }) => ({ + width: '12px', + height: '12px', + borderRadius: '50%', + backgroundColor: $color, + marginRight: theme.spacing(0.2), + marginLeft: theme.spacing(1.5), + border: + $color === '#FFFFFF' + ? `1px solid ${theme.palette.divider}` + : `1px solid ${$color}`, + }), +); export const TagTypeList = () => { const [deletion, setDeletion] = useState<{ @@ -41,6 +57,7 @@ export const TagTypeList = () => { const { deleteTagType } = useTagTypesApi(); const { tagTypes, refetch, loading } = useTagTypes(); const { setToastData, setToastApiError } = useToast(); + const isTagTypeColorEnabled = Boolean(useUiFlag('tagTypeColor')); const data = useMemo(() => { if (loading) { @@ -50,9 +67,10 @@ export const TagTypeList = () => { }); } - return tagTypes.map(({ name, description }) => ({ + return tagTypes.map(({ name, description, color }) => ({ name, description, + color, })); }, [tagTypes, loading]); @@ -81,15 +99,23 @@ export const TagTypeList = () => { width: '90%', Cell: ({ row: { - original: { name, description }, + original: { name, description, color }, }, }: any) => { return ( - + + } + /> + + ); }, sortType: 'alphanumeric', @@ -136,7 +162,7 @@ export const TagTypeList = () => { disableSortBy: true, }, ], - [navigate], + [navigate, isTagTypeColorEnabled], ); const initialState = useMemo( diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index 116f059d28..87b5f111ca 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -266,29 +266,33 @@ exports[`renders an empty list correctly 1`] = ` role="cell" >
- + - Tag type name - - - Tag type description when loading - + > + Tag type name + + + Tag type description when loading + +
@@ -436,29 +440,33 @@ exports[`renders an empty list correctly 1`] = ` role="cell" >
- + - Tag type name - - - Tag type description when loading - + > + Tag type name + + + Tag type description when loading + +
@@ -606,29 +614,33 @@ exports[`renders an empty list correctly 1`] = ` role="cell" >
- + - Tag type name - - - Tag type description when loading - + > + Tag type name + + + Tag type description when loading + +
@@ -776,29 +788,33 @@ exports[`renders an empty list correctly 1`] = ` role="cell" >
- + - Tag type name - - - Tag type description when loading - + > + Tag type name + + + Tag type description when loading + +
@@ -946,29 +962,33 @@ exports[`renders an empty list correctly 1`] = ` role="cell" >
- + - Tag type name - - - Tag type description when loading - + > + Tag type name + + + Tag type description when loading + +
diff --git a/frontend/src/interfaces/tags.ts b/frontend/src/interfaces/tags.ts index 8ceda3b21f..21d4c50985 100644 --- a/frontend/src/interfaces/tags.ts +++ b/frontend/src/interfaces/tags.ts @@ -7,9 +7,11 @@ export interface ITagType { name: string; description: string; icon: string; + color?: string; } export interface ITagPayload { name: string; description: string; + color?: string; } diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index fd763f90b6..ce6b3be37b 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -92,6 +92,7 @@ export type UiFlags = { consumptionModel?: boolean; edgeObservability?: boolean; adminNavUI?: boolean; + tagTypeColor?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 62bab00caf..e9bd25799e 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -65,7 +65,8 @@ export type IFlagKey = | 'teamsIntegrationChangeRequests' | 'edgeObservability' | 'simplifyDisableFeature' - | 'adminNavUI'; + | 'adminNavUI' + | 'tagTypeColor'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -314,6 +315,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_ADMIN_NAV_UI, false, ), + tagTypeColor: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_TAG_TYPE_COLOR, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 5dfa0b7bc0..311d0efabc 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -59,6 +59,7 @@ process.nextTick(async () => { teamsIntegrationChangeRequests: true, simplifyDisableFeature: true, adminNavUI: false, + tagTypeColor: true, }, }, authentication: {