From aa6c422165636168e4e764dc5288644d12741d08 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Mon, 31 Mar 2025 11:55:49 +0200 Subject: [PATCH] Feat/tag type frontend display (#9630) Add frontend for displaying tag colors --- .../FeatureOverviewCell.tsx | 162 ++++++++++++------ .../cells/FeatureTagCell/FeatureTagCell.tsx | 10 +- frontend/src/component/common/Tag/Tag.tsx | 88 ++++++++++ .../FeatureToggleFilters.tsx | 5 +- .../FeatureOverviewMetaData/TagRow.tsx | 124 +++++++++----- .../FeatureOverviewSidePanelTags.tsx | 3 +- .../ManageTagsDialog/ManageBulkTagsDialog.tsx | 19 +- .../ManageTagsDialog/TagsInput.tsx | 5 +- .../CreateFeatureDialog.tsx | 3 +- .../ProjectOverviewFilters.tsx | 12 +- .../ManageTags.tsx | 8 +- frontend/src/interfaces/tags.ts | 1 + frontend/src/openapi/models/tagSchema.ts | 6 + frontend/src/utils/format-tag.ts | 8 + 14 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 frontend/src/component/common/Tag/Tag.tsx create mode 100644 frontend/src/utils/format-tag.ts diff --git a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx index c20d6096e8..1a1806f785 100644 --- a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx @@ -1,6 +1,9 @@ -import type { FC, ReactElement } from 'react'; -import type { FeatureSearchResponseSchema } from '../../../../../openapi'; -import { Box, IconButton, styled } from '@mui/material'; +import type { FC } from 'react'; +import type { + FeatureSearchResponseSchema, + TagSchema, +} from '../../../../../openapi'; +import { Box, IconButton, styled, Chip } from '@mui/material'; import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext'; @@ -13,6 +16,9 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { getLocalizedDateString } from '../../../util'; +import { Tag } from 'component/common/Tag/Tag'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { formatTag } from 'utils/format-tag'; interface IFeatureNameCellProps { row: { @@ -36,7 +42,7 @@ const StyledFeatureLink = styled(Link)({ }, }); -const Tag = styled('button')(({ theme }) => ({ +const CustomTagButton = styled('button')(({ theme }) => ({ marginRight: theme.spacing(0.5), border: `1px solid ${theme.palette.divider}`, borderRadius: theme.shape.borderRadius, @@ -51,6 +57,24 @@ const Tag = styled('button')(({ theme }) => ({ color: 'inherit', })); +const StyledTag = styled(Chip)(({ theme }) => ({ + overflowWrap: 'anywhere', + lineHeight: theme.typography.body1.lineHeight, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + padding: theme.spacing(0.25, 0.5), + height: 'auto', + fontSize: theme.fontSizes.smallerBody, + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + '&:hover': { + backgroundColor: theme.palette.background.paper, + }, + '& .MuiChip-label': { + padding: 0, + }, +})); + const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ text, searchQuery, @@ -80,19 +104,6 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ ); }; -const CappedTag: FC<{ tag: string; children: ReactElement }> = ({ - tag, - children, -}) => { - return ( - 30} - show={{children}} - elseShow={children} - /> - ); -}; - const Container = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', @@ -158,23 +169,82 @@ const ArchivedFeatureName: FC<{ ); }; -const RestTags: FC<{ tags: string[]; onClick: (tag: string) => void }> = ({ - tags, - onClick, -}) => { +interface ITagItemProps { + tag: TagSchema; + onClick: (tag: TagSchema) => void; +} + +const TagItem: FC = ({ tag, onClick }) => { + const isTagTypeColorEnabled = useUiFlag('tagTypeColor'); + const tagFullText = formatTag(tag); + + if (isTagTypeColorEnabled) { + const tagComponent = ( + onClick(tag)} sx={{ cursor: 'pointer' }}> + + + ); + + return ( + + {tagComponent} + + ); + } + + // For non-color tags, use the StyledTag approach + const isOverflowing = tagFullText.length > 30; + const displayText = isOverflowing + ? `${tagFullText.substring(0, 30)}...` + : tagFullText; + + return ( + onClick(tag)} + sx={{ cursor: 'pointer' }} + title={isOverflowing ? tagFullText : undefined} + /> + ); +}; + +const RestTags: FC<{ + tags: TagSchema[]; + onClick: (tag: string) => void; +}> = ({ tags, onClick }) => { + const isTagTypeColorEnabled = useUiFlag('tagTypeColor'); + return ( ( - onClick(tag)} - key={tag} - > - {tag} - - ))} + title={tags.map((tag) => { + const formattedTag = formatTag(tag); + return ( + onClick(formattedTag)} + key={formattedTag} + > + {isTagTypeColorEnabled ? ( + + ) : ( + formattedTag + )} + + ); + })} > - {tags.length} more... + theme.spacing(2), + }), + }} + > + {tags.length} more... + ); }; @@ -183,27 +253,21 @@ const Tags: FC<{ tags: FeatureSearchResponseSchema['tags']; onClick: (tag: string) => void; }> = ({ tags, onClick }) => { - const [tag1, tag2, tag3, ...restTags] = (tags || []).map( - ({ type, value }) => `${type}:${value}`, - ); + if (!tags || tags.length === 0) { + return null; + } + + const [tag1, tag2, tag3, ...restTags] = tags; + + const handleTagClick = (tag: TagSchema) => { + onClick(formatTag(tag)); + }; return ( - {tag1 && ( - - onClick(tag1)}>{tag1} - - )} - {tag2 && ( - - onClick(tag2)}>{tag2} - - )} - {tag3 && ( - - onClick(tag3)}>{tag3} - - )} + {tag1 && } + {tag2 && } + {tag3 && } 0} show={} diff --git a/frontend/src/component/common/Table/cells/FeatureTagCell/FeatureTagCell.tsx b/frontend/src/component/common/Table/cells/FeatureTagCell/FeatureTagCell.tsx index 19e370f7a9..2fa4abb289 100644 --- a/frontend/src/component/common/Table/cells/FeatureTagCell/FeatureTagCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureTagCell/FeatureTagCell.tsx @@ -1,10 +1,11 @@ import type { VFC } from 'react'; -import type { FeatureSchema } from 'openapi'; +import type { FeatureSchema, TagSchema } from 'openapi'; import { styled, Typography } from '@mui/material'; import { TextCell } from '../TextCell/TextCell'; import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; +import { formatTag } from 'utils/format-tag'; const StyledTag = styled(Typography)(({ theme }) => ({ fontSize: theme.fontSizes.smallerBody, @@ -23,9 +24,8 @@ export const FeatureTagCell: VFC = ({ row }) => { return ; const value = - row.original.tags - ?.map(({ type, value }) => `${type}:${value}`) - .join('\n') || ''; + row.original.tags?.map((tag: TagSchema) => formatTag(tag)).join('\n') || + ''; return ( @@ -39,7 +39,7 @@ export const FeatureTagCell: VFC = ({ row }) => { {row.original.tags?.map((tag) => ( - {`${tag.type}:${tag.value}`} + {formatTag(tag)} ))} diff --git a/frontend/src/component/common/Tag/Tag.tsx b/frontend/src/component/common/Tag/Tag.tsx new file mode 100644 index 0000000000..b689e20932 --- /dev/null +++ b/frontend/src/component/common/Tag/Tag.tsx @@ -0,0 +1,88 @@ +import { styled } from '@mui/material'; +import { Chip } from '@mui/material'; +import type { TagSchema } from 'openapi'; +import type { ReactElement } from 'react'; +import { formatTag } from 'utils/format-tag'; + +const StyledChip = styled(Chip)<{ $color?: string }>(({ theme, $color }) => ({ + borderRadius: theme.spacing(2), + border: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + height: '26px', + '& .MuiChip-label': { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + '& .MuiChip-deleteIcon': { + marginLeft: theme.spacing(0.5), + marginRight: theme.spacing(0.5), + }, +})); + +const ColorDot = styled('div')<{ $color: string }>(({ theme, $color }) => ({ + width: theme.spacing(1.3), + height: theme.spacing(1.3), + borderRadius: '50%', + backgroundColor: $color, + border: `1px solid ${$color === '#FFFFFF' ? theme.palette.divider : $color}`, + flexShrink: 0, +})); + +interface ITagProps { + tag: TagSchema; + onDelete?: () => void; + deleteIcon?: ReactElement; + maxLength?: number; +} + +export const Tag = ({ + tag, + onDelete, + deleteIcon, + maxLength = 30, +}: ITagProps) => { + const fullLabel = formatTag(tag); + const isOverflowing = fullLabel.length > maxLength; + const showColorDot = tag.color && tag.color !== '#FFFFFF'; + + let displayValue = tag.value; + if (isOverflowing) { + // Calculate how much of the value we can show + const maxValueLength = Math.max(0, maxLength - tag.type.length - 1); // -1 for the colon + displayValue = `${tag.value.substring(0, maxValueLength)}...`; + } + + const labelContent = ( + + {showColorDot && } + + {`${tag.type}:${displayValue}`} + + + ) as ReactElement; + + return ( + + ); +}; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx index 833722b782..f605182b83 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -7,6 +7,7 @@ import { Filters, type IFilterItem, } from 'component/filter/Filters/Filters'; +import { formatTag } from 'utils/format-tag'; interface IFeatureToggleFiltersProps { state: FilterItemParamHolder; @@ -44,8 +45,8 @@ export const FeatureToggleFilters: VFC = ({ value: segment.name, })); const tagsOptions = (tags || []).map((tag) => ({ - label: `${tag.type}:${tag.value}`, - value: `${tag.type}:${tag.value}`, + label: formatTag(tag), + value: formatTag(tag), })); const hasMultipleProjects = projectsOptions.length > 1; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx index 788354d316..6d12af00c7 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx @@ -1,8 +1,9 @@ import type { IFeatureToggle } from 'interfaces/featureToggle'; import { useContext, useState } from 'react'; -import { Chip, styled, Tooltip } from '@mui/material'; +import { styled, Tooltip, Chip } from '@mui/material'; import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags'; import DeleteTagIcon from '@mui/icons-material/Cancel'; +import ClearIcon from '@mui/icons-material/Clear'; import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog'; import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import AccessContext from 'contexts/AccessContext'; @@ -13,6 +14,9 @@ import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { StyledMetaDataItem } from './FeatureOverviewMetaData'; import { AddTagButton } from './AddTagButton'; +import { Tag } from 'component/common/Tag/Tag'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { formatTag } from 'utils/format-tag'; const StyledLabel = styled('span')(({ theme }) => ({ marginTop: theme.spacing(1), @@ -56,7 +60,6 @@ interface IFeatureOverviewSidePanelTagsProps { export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { const { tags, refetch } = useFeatureTags(feature.name); const { deleteTagFromFeature } = useFeatureApi(); - const [manageTagsOpen, setManageTagsOpen] = useState(false); const [removeTagOpen, setRemoveTagOpen] = useState(false); const [selectedTag, setSelectedTag] = useState(); @@ -103,49 +106,17 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { Tags: - {tags.map((tag) => { - const tagLabel = `${tag.type}:${tag.value}`; - const isOverflowing = tagLabel.length > 25; - return ( - - - {tagLabel.substring(0, 25)} - {isOverflowing ? ( - - … - - ) : ( - '' - )} - - - } - size='small' - deleteIcon={ - - - - } - onDelete={ - canUpdateTags - ? () => { - setRemoveTagOpen(true); - setSelectedTag(tag); - } - : undefined - } - /> - ); - })} + {tags.map((tag) => ( + { + setRemoveTagOpen(true); + setSelectedTag(tag); + }} + /> + ))} {canUpdateTags ? ( { ); }; + +interface ITagItemProps { + tag: ITag; + canUpdateTags: boolean; + onTagRemove: (tag: ITag) => void; +} + +const TagItem = ({ tag, canUpdateTags, onTagRemove }: ITagItemProps) => { + const isTagTypeColorEnabled = useUiFlag('tagTypeColor'); + const tagLabel = formatTag(tag); + const isOverflowing = tagLabel.length > 25; + const deleteIcon = ( + + + + ); + const onDelete = canUpdateTags ? () => onTagRemove(tag) : undefined; + + if (isTagTypeColorEnabled) { + const deleteIcon = ( + + + + ); + + return ( + + + + + + ); + } + + return ( + + + {tagLabel.substring(0, 25)} + {isOverflowing ? ( + + ) : ( + '' + )} + + + } + size='small' + deleteIcon={deleteIcon} + onDelete={onDelete} + /> + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx index 6a91257fd2..442ae41ff8 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx @@ -13,6 +13,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { formatTag } from 'utils/format-tag'; const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -83,7 +84,7 @@ export const FeatureOverviewSidePanelTags = ({ {header} {tags.map((tag) => { - const tagLabel = `${tag.type}:${tag.value}`; + const tagLabel = formatTag(tag); return ( void; onSubmit: (payload: Payload) => void; } @@ -36,14 +37,14 @@ const StyledDialogFormContent = styled('section')(({ theme }) => ({ const formId = 'manage-tags-form'; -const mergeTags = (tags: ITag[], newTag: ITag) => [ +const mergeTags = (tags: TagSchema[], newTag: TagSchema) => [ ...tags, ...(tags.some((x) => x.value === newTag.value && x.type === newTag.type) ? [] : [newTag]), ]; -const filterTags = (tags: ITag[], tag: ITag) => +const filterTags = (tags: TagSchema[], tag: TagSchema) => tags.filter((x) => !(x.value === tag.value && x.type === tag.type)); export const payloadReducer = ( @@ -51,11 +52,11 @@ export const payloadReducer = ( action: | { type: 'add' | 'remove'; - payload: ITag; + payload: TagSchema; } | { type: 'clear'; - payload: ITag[]; + payload: TagSchema[]; } | { type: 'reset' }, ) => { @@ -171,7 +172,7 @@ export const ManageBulkTagsDialog: FC = ({ value, type, }).then(async () => { - await refetchTags(); + refetchTags(); setSelectedTags((prev) => [...prev, { title: value }]); dispatch({ type: 'add', diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx index f41794658f..be7532b7da 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx @@ -10,9 +10,10 @@ import type React from 'react'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'; -import type { ITag, ITagType } from 'interfaces/tags'; +import type { ITagType } from 'interfaces/tags'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import Add from '@mui/icons-material/Add'; +import type { TagSchema } from 'openapi'; export type TagOption = { title: string; @@ -21,7 +22,7 @@ export type TagOption = { interface ITagsInputProps { options: TagOption[]; - existingTags: ITag[]; + existingTags: TagSchema[]; tagType: ITagType; selectedOptions: TagOption[]; indeterminateOptions?: TagOption[]; diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx index 689879f650..3b2179be4f 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx @@ -30,6 +30,7 @@ import type { ITag } from 'interfaces/tags'; import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton'; import { useFlagLimits } from './useFlagLimits'; import { useFeatureCreatedFeedback } from './hooks/useFeatureCreatedFeedback'; +import { formatTag } from 'utils/format-tag'; interface ICreateFeatureDialogProps { open: boolean; @@ -295,7 +296,7 @@ const CreateFeatureDialogContent = ({ description={configButtonData.tags.text} selectedOptions={tags} options={allTags.map((tag) => ({ - label: `${tag.type}:${tag.value}`, + label: formatTag(tag), value: tag, }))} onChange={setTags} diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx index 71fa764edd..8380743795 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx @@ -6,6 +6,7 @@ import { type IFilterItem, } from 'component/filter/Filters/Filters'; import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators'; +import { formatTag } from 'utils/format-tag'; interface IProjectOverviewFilters { state: FilterItemParamHolder; @@ -23,10 +24,13 @@ export const ProjectOverviewFilters: VFC = ({ const [availableFilters, setAvailableFilters] = useState([]); useEffect(() => { - const tagsOptions = (tags || []).map((tag) => ({ - label: `${tag.type}:${tag.value}`, - value: `${tag.type}:${tag.value}`, - })); + const tagsOptions = (tags || []).map((tag) => { + const tagString = formatTag(tag); + return { + label: tagString, + value: tagString, + }; + }); const flagCreatorsOptions = flagCreators.map((creator) => ({ label: creator.name, diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx index cc2bb45d2e..67c34a9fc1 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, type VFC } from 'react'; import { Button } from '@mui/material'; import { ManageBulkTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog'; -import type { FeatureSchema } from 'openapi'; +import type { FeatureSchema, TagSchema } from 'openapi'; import type { ITag } from 'interfaces/tags'; import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; import useToast from 'hooks/useToast'; @@ -28,7 +28,7 @@ export const ManageTags: VFC = ({ const [initialValues, indeterminateValues] = useMemo(() => { const uniqueTags = data .flatMap(({ tags }) => tags || []) - .reduce( + .reduce( (acc, tag) => [ ...acc, ...(acc.some( @@ -56,8 +56,8 @@ export const ManageTags: VFC = ({ addedTags, removedTags, }: { - addedTags: ITag[]; - removedTags: ITag[]; + addedTags: TagSchema[]; + removedTags: TagSchema[]; }) => { const features = data.map(({ name }) => name); const payload = { features, tags: { addedTags, removedTags } }; diff --git a/frontend/src/interfaces/tags.ts b/frontend/src/interfaces/tags.ts index 21d4c50985..3c0429c939 100644 --- a/frontend/src/interfaces/tags.ts +++ b/frontend/src/interfaces/tags.ts @@ -1,6 +1,7 @@ export interface ITag { value: string; type: string; + color?: string; } export interface ITagType { diff --git a/frontend/src/openapi/models/tagSchema.ts b/frontend/src/openapi/models/tagSchema.ts index 397d19c557..6283307419 100644 --- a/frontend/src/openapi/models/tagSchema.ts +++ b/frontend/src/openapi/models/tagSchema.ts @@ -8,6 +8,12 @@ * Representation of a [tag](https://docs.getunleash.io/reference/feature-toggles#tags) */ export interface TagSchema { + /** + * The hexadecimal color code for the tag type. + * @nullable + * @pattern ^#[0-9A-Fa-f]{6}$ + */ + color?: string | null; /** * The [type](https://docs.getunleash.io/reference/feature-toggles#tags) of the tag * @minLength 2 diff --git a/frontend/src/utils/format-tag.ts b/frontend/src/utils/format-tag.ts new file mode 100644 index 0000000000..56aaca492c --- /dev/null +++ b/frontend/src/utils/format-tag.ts @@ -0,0 +1,8 @@ +import type { ITag } from 'interfaces/tags'; +import type { TagSchema } from 'openapi'; + +// Use TagSchema or ITag for backwards compatability in components that +// have not yet been refactored to use TagSchema +export const formatTag = (tag: TagSchema | ITag) => { + return `${tag.type}:${tag.value}`; +};