mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat/tag type frontend display (#9630)
Add frontend for displaying tag colors
This commit is contained in:
		
							parent
							
								
									9de0e7435b
								
							
						
					
					
						commit
						aa6c422165
					
				| @ -1,6 +1,9 @@ | |||||||
| import type { FC, ReactElement } from 'react'; | import type { FC } from 'react'; | ||||||
| import type { FeatureSearchResponseSchema } from '../../../../../openapi'; | import type { | ||||||
| import { Box, IconButton, styled } from '@mui/material'; |     FeatureSearchResponseSchema, | ||||||
|  |     TagSchema, | ||||||
|  | } from '../../../../../openapi'; | ||||||
|  | import { Box, IconButton, styled, Chip } from '@mui/material'; | ||||||
| import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; | import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; | ||||||
| import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||||
| import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext'; | 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 { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||||
| import { getLocalizedDateString } from '../../../util'; | import { getLocalizedDateString } from '../../../util'; | ||||||
|  | import { Tag } from 'component/common/Tag/Tag'; | ||||||
|  | import { useUiFlag } from 'hooks/useUiFlag'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| interface IFeatureNameCellProps { | interface IFeatureNameCellProps { | ||||||
|     row: { |     row: { | ||||||
| @ -36,7 +42,7 @@ const StyledFeatureLink = styled(Link)({ | |||||||
|     }, |     }, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const Tag = styled('button')(({ theme }) => ({ | const CustomTagButton = styled('button')(({ theme }) => ({ | ||||||
|     marginRight: theme.spacing(0.5), |     marginRight: theme.spacing(0.5), | ||||||
|     border: `1px solid ${theme.palette.divider}`, |     border: `1px solid ${theme.palette.divider}`, | ||||||
|     borderRadius: theme.shape.borderRadius, |     borderRadius: theme.shape.borderRadius, | ||||||
| @ -51,6 +57,24 @@ const Tag = styled('button')(({ theme }) => ({ | |||||||
|     color: 'inherit', |     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 }> = ({ | const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ | ||||||
|     text, |     text, | ||||||
|     searchQuery, |     searchQuery, | ||||||
| @ -80,19 +104,6 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ | |||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const CappedTag: FC<{ tag: string; children: ReactElement }> = ({ |  | ||||||
|     tag, |  | ||||||
|     children, |  | ||||||
| }) => { |  | ||||||
|     return ( |  | ||||||
|         <ConditionallyRender |  | ||||||
|             condition={tag.length > 30} |  | ||||||
|             show={<HtmlTooltip title={tag}>{children}</HtmlTooltip>} |  | ||||||
|             elseShow={children} |  | ||||||
|         /> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const Container = styled(Box)(({ theme }) => ({ | const Container = styled(Box)(({ theme }) => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
|     flexDirection: 'column', |     flexDirection: 'column', | ||||||
| @ -158,23 +169,82 @@ const ArchivedFeatureName: FC<{ | |||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const RestTags: FC<{ tags: string[]; onClick: (tag: string) => void }> = ({ | interface ITagItemProps { | ||||||
|     tags, |     tag: TagSchema; | ||||||
|     onClick, |     onClick: (tag: TagSchema) => void; | ||||||
| }) => { | } | ||||||
|  | 
 | ||||||
|  | const TagItem: FC<ITagItemProps> = ({ tag, onClick }) => { | ||||||
|  |     const isTagTypeColorEnabled = useUiFlag('tagTypeColor'); | ||||||
|  |     const tagFullText = formatTag(tag); | ||||||
|  | 
 | ||||||
|  |     if (isTagTypeColorEnabled) { | ||||||
|  |         const tagComponent = ( | ||||||
|  |             <Box onClick={() => onClick(tag)} sx={{ cursor: 'pointer' }}> | ||||||
|  |                 <Tag tag={tag} maxLength={30} /> | ||||||
|  |             </Box> | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             <HtmlTooltip key={tagFullText} title={tagFullText} arrow> | ||||||
|  |                 <span>{tagComponent}</span> | ||||||
|  |             </HtmlTooltip> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // For non-color tags, use the StyledTag approach
 | ||||||
|  |     const isOverflowing = tagFullText.length > 30; | ||||||
|  |     const displayText = isOverflowing | ||||||
|  |         ? `${tagFullText.substring(0, 30)}...` | ||||||
|  |         : tagFullText; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledTag | ||||||
|  |             key={tagFullText} | ||||||
|  |             label={displayText} | ||||||
|  |             size='small' | ||||||
|  |             onClick={() => 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 ( |     return ( | ||||||
|         <HtmlTooltip |         <HtmlTooltip | ||||||
|             title={tags.map((tag) => ( |             title={tags.map((tag) => { | ||||||
|  |                 const formattedTag = formatTag(tag); | ||||||
|  |                 return ( | ||||||
|                     <Box |                     <Box | ||||||
|                         sx={{ cursor: 'pointer' }} |                         sx={{ cursor: 'pointer' }} | ||||||
|                     onClick={() => onClick(tag)} |                         onClick={() => onClick(formattedTag)} | ||||||
|                     key={tag} |                         key={formattedTag} | ||||||
|                     > |                     > | ||||||
|                     {tag} |                         {isTagTypeColorEnabled ? ( | ||||||
|  |                             <Tag tag={tag} maxLength={30} /> | ||||||
|  |                         ) : ( | ||||||
|  |                             formattedTag | ||||||
|  |                         )} | ||||||
|                     </Box> |                     </Box> | ||||||
|             ))} |                 ); | ||||||
|  |             })} | ||||||
|         > |         > | ||||||
|             <Tag sx={{ cursor: 'initial' }}>{tags.length} more...</Tag> |             <CustomTagButton | ||||||
|  |                 sx={{ | ||||||
|  |                     cursor: 'initial', | ||||||
|  |                     ...(isTagTypeColorEnabled && { | ||||||
|  |                         borderRadius: (theme) => theme.spacing(2), | ||||||
|  |                     }), | ||||||
|  |                 }} | ||||||
|  |             > | ||||||
|  |                 {tags.length} more... | ||||||
|  |             </CustomTagButton> | ||||||
|         </HtmlTooltip> |         </HtmlTooltip> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| @ -183,27 +253,21 @@ const Tags: FC<{ | |||||||
|     tags: FeatureSearchResponseSchema['tags']; |     tags: FeatureSearchResponseSchema['tags']; | ||||||
|     onClick: (tag: string) => void; |     onClick: (tag: string) => void; | ||||||
| }> = ({ tags, onClick }) => { | }> = ({ tags, onClick }) => { | ||||||
|     const [tag1, tag2, tag3, ...restTags] = (tags || []).map( |     if (!tags || tags.length === 0) { | ||||||
|         ({ type, value }) => `${type}:${value}`, |         return null; | ||||||
|     ); |     } | ||||||
|  | 
 | ||||||
|  |     const [tag1, tag2, tag3, ...restTags] = tags; | ||||||
|  | 
 | ||||||
|  |     const handleTagClick = (tag: TagSchema) => { | ||||||
|  |         onClick(formatTag(tag)); | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <TagsContainer> |         <TagsContainer> | ||||||
|             {tag1 && ( |             {tag1 && <TagItem tag={tag1} onClick={handleTagClick} />} | ||||||
|                 <CappedTag tag={tag1}> |             {tag2 && <TagItem tag={tag2} onClick={handleTagClick} />} | ||||||
|                     <Tag onClick={() => onClick(tag1)}>{tag1}</Tag> |             {tag3 && <TagItem tag={tag3} onClick={handleTagClick} />} | ||||||
|                 </CappedTag> |  | ||||||
|             )} |  | ||||||
|             {tag2 && ( |  | ||||||
|                 <CappedTag tag={tag2}> |  | ||||||
|                     <Tag onClick={() => onClick(tag2)}>{tag2}</Tag> |  | ||||||
|                 </CappedTag> |  | ||||||
|             )} |  | ||||||
|             {tag3 && ( |  | ||||||
|                 <CappedTag tag={tag3}> |  | ||||||
|                     <Tag onClick={() => onClick(tag3)}>{tag3}</Tag> |  | ||||||
|                 </CappedTag> |  | ||||||
|             )} |  | ||||||
|             <ConditionallyRender |             <ConditionallyRender | ||||||
|                 condition={restTags.length > 0} |                 condition={restTags.length > 0} | ||||||
|                 show={<RestTags tags={restTags} onClick={onClick} />} |                 show={<RestTags tags={restTags} onClick={onClick} />} | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| import type { VFC } from 'react'; | import type { VFC } from 'react'; | ||||||
| import type { FeatureSchema } from 'openapi'; | import type { FeatureSchema, TagSchema } from 'openapi'; | ||||||
| import { styled, Typography } from '@mui/material'; | import { styled, Typography } from '@mui/material'; | ||||||
| import { TextCell } from '../TextCell/TextCell'; | import { TextCell } from '../TextCell/TextCell'; | ||||||
| import { Highlighter } from 'component/common/Highlighter/Highlighter'; | import { Highlighter } from 'component/common/Highlighter/Highlighter'; | ||||||
| import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||||
| import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; | import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| const StyledTag = styled(Typography)(({ theme }) => ({ | const StyledTag = styled(Typography)(({ theme }) => ({ | ||||||
|     fontSize: theme.fontSizes.smallerBody, |     fontSize: theme.fontSizes.smallerBody, | ||||||
| @ -23,9 +24,8 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row }) => { | |||||||
|         return <TextCell />; |         return <TextCell />; | ||||||
| 
 | 
 | ||||||
|     const value = |     const value = | ||||||
|         row.original.tags |         row.original.tags?.map((tag: TagSchema) => formatTag(tag)).join('\n') || | ||||||
|             ?.map(({ type, value }) => `${type}:${value}`) |         ''; | ||||||
|             .join('\n') || ''; |  | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <TextCell> |         <TextCell> | ||||||
| @ -39,7 +39,7 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row }) => { | |||||||
|                         {row.original.tags?.map((tag) => ( |                         {row.original.tags?.map((tag) => ( | ||||||
|                             <StyledTag key={tag.type + tag.value}> |                             <StyledTag key={tag.type + tag.value}> | ||||||
|                                 <Highlighter search={searchQuery}> |                                 <Highlighter search={searchQuery}> | ||||||
|                                     {`${tag.type}:${tag.value}`} |                                     {formatTag(tag)} | ||||||
|                                 </Highlighter> |                                 </Highlighter> | ||||||
|                             </StyledTag> |                             </StyledTag> | ||||||
|                         ))} |                         ))} | ||||||
|  | |||||||
							
								
								
									
										88
									
								
								frontend/src/component/common/Tag/Tag.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								frontend/src/component/common/Tag/Tag.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 = ( | ||||||
|  |         <span | ||||||
|  |             style={{ | ||||||
|  |                 display: 'flex', | ||||||
|  |                 alignItems: 'center', | ||||||
|  |                 gap: '8px', | ||||||
|  |                 width: '100%', | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             {showColorDot && <ColorDot $color={tag.color!} />} | ||||||
|  |             <span | ||||||
|  |                 style={{ | ||||||
|  |                     overflow: 'hidden', | ||||||
|  |                     textOverflow: 'ellipsis', | ||||||
|  |                     whiteSpace: 'nowrap', | ||||||
|  |                 }} | ||||||
|  |             > | ||||||
|  |                 {`${tag.type}:${displayValue}`} | ||||||
|  |             </span> | ||||||
|  |         </span> | ||||||
|  |     ) as ReactElement; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledChip | ||||||
|  |             label={labelContent} | ||||||
|  |             onDelete={onDelete} | ||||||
|  |             deleteIcon={deleteIcon} | ||||||
|  |             title={isOverflowing ? fullLabel : undefined} | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -7,6 +7,7 @@ import { | |||||||
|     Filters, |     Filters, | ||||||
|     type IFilterItem, |     type IFilterItem, | ||||||
| } from 'component/filter/Filters/Filters'; | } from 'component/filter/Filters/Filters'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| interface IFeatureToggleFiltersProps { | interface IFeatureToggleFiltersProps { | ||||||
|     state: FilterItemParamHolder; |     state: FilterItemParamHolder; | ||||||
| @ -44,8 +45,8 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({ | |||||||
|             value: segment.name, |             value: segment.name, | ||||||
|         })); |         })); | ||||||
|         const tagsOptions = (tags || []).map((tag) => ({ |         const tagsOptions = (tags || []).map((tag) => ({ | ||||||
|             label: `${tag.type}:${tag.value}`, |             label: formatTag(tag), | ||||||
|             value: `${tag.type}:${tag.value}`, |             value: formatTag(tag), | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         const hasMultipleProjects = projectsOptions.length > 1; |         const hasMultipleProjects = projectsOptions.length > 1; | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| import type { IFeatureToggle } from 'interfaces/featureToggle'; | import type { IFeatureToggle } from 'interfaces/featureToggle'; | ||||||
| import { useContext, useState } from 'react'; | 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 useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags'; | ||||||
| import DeleteTagIcon from '@mui/icons-material/Cancel'; | import DeleteTagIcon from '@mui/icons-material/Cancel'; | ||||||
|  | import ClearIcon from '@mui/icons-material/Clear'; | ||||||
| import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog'; | import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog'; | ||||||
| import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; | import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; | ||||||
| import AccessContext from 'contexts/AccessContext'; | import AccessContext from 'contexts/AccessContext'; | ||||||
| @ -13,6 +14,9 @@ import useToast from 'hooks/useToast'; | |||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { StyledMetaDataItem } from './FeatureOverviewMetaData'; | import { StyledMetaDataItem } from './FeatureOverviewMetaData'; | ||||||
| import { AddTagButton } from './AddTagButton'; | 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 }) => ({ | const StyledLabel = styled('span')(({ theme }) => ({ | ||||||
|     marginTop: theme.spacing(1), |     marginTop: theme.spacing(1), | ||||||
| @ -56,7 +60,6 @@ interface IFeatureOverviewSidePanelTagsProps { | |||||||
| export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { | export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { | ||||||
|     const { tags, refetch } = useFeatureTags(feature.name); |     const { tags, refetch } = useFeatureTags(feature.name); | ||||||
|     const { deleteTagFromFeature } = useFeatureApi(); |     const { deleteTagFromFeature } = useFeatureApi(); | ||||||
| 
 |  | ||||||
|     const [manageTagsOpen, setManageTagsOpen] = useState(false); |     const [manageTagsOpen, setManageTagsOpen] = useState(false); | ||||||
|     const [removeTagOpen, setRemoveTagOpen] = useState(false); |     const [removeTagOpen, setRemoveTagOpen] = useState(false); | ||||||
|     const [selectedTag, setSelectedTag] = useState<ITag>(); |     const [selectedTag, setSelectedTag] = useState<ITag>(); | ||||||
| @ -103,49 +106,17 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { | |||||||
|                 <StyledTagRow> |                 <StyledTagRow> | ||||||
|                     <StyledLabel>Tags:</StyledLabel> |                     <StyledLabel>Tags:</StyledLabel> | ||||||
|                     <StyledTagContainer> |                     <StyledTagContainer> | ||||||
|                         {tags.map((tag) => { |                         {tags.map((tag) => ( | ||||||
|                             const tagLabel = `${tag.type}:${tag.value}`; |                             <TagItem | ||||||
|                             const isOverflowing = tagLabel.length > 25; |                                 key={formatTag(tag)} | ||||||
|                             return ( |                                 tag={tag} | ||||||
|                                 <StyledTag |                                 canUpdateTags={canUpdateTags} | ||||||
|                                     key={tagLabel} |                                 onTagRemove={(tag) => { | ||||||
|                                     label={ |  | ||||||
|                                         <Tooltip |  | ||||||
|                                             key={tagLabel} |  | ||||||
|                                             title={ |  | ||||||
|                                                 isOverflowing ? tagLabel : '' |  | ||||||
|                                             } |  | ||||||
|                                             arrow |  | ||||||
|                                         > |  | ||||||
|                                             <span> |  | ||||||
|                                                 {tagLabel.substring(0, 25)} |  | ||||||
|                                                 {isOverflowing ? ( |  | ||||||
|                                                     <StyledEllipsis> |  | ||||||
|                                                         … |  | ||||||
|                                                     </StyledEllipsis> |  | ||||||
|                                                 ) : ( |  | ||||||
|                                                     '' |  | ||||||
|                                                 )} |  | ||||||
|                                             </span> |  | ||||||
|                                         </Tooltip> |  | ||||||
|                                     } |  | ||||||
|                                     size='small' |  | ||||||
|                                     deleteIcon={ |  | ||||||
|                                         <Tooltip title='Remove tag' arrow> |  | ||||||
|                                             <DeleteTagIcon /> |  | ||||||
|                                         </Tooltip> |  | ||||||
|                                     } |  | ||||||
|                                     onDelete={ |  | ||||||
|                                         canUpdateTags |  | ||||||
|                                             ? () => { |  | ||||||
|                                     setRemoveTagOpen(true); |                                     setRemoveTagOpen(true); | ||||||
|                                     setSelectedTag(tag); |                                     setSelectedTag(tag); | ||||||
|                                               } |                                 }} | ||||||
|                                             : undefined |  | ||||||
|                                     } |  | ||||||
|                             /> |                             /> | ||||||
|                             ); |                         ))} | ||||||
|                         })} |  | ||||||
|                         {canUpdateTags ? ( |                         {canUpdateTags ? ( | ||||||
|                             <AddTagButton |                             <AddTagButton | ||||||
|                                 project={feature.project} |                                 project={feature.project} | ||||||
| @ -183,3 +154,66 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { | |||||||
|         </> |         </> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | 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 = ( | ||||||
|  |         <Tooltip title='Remove tag' arrow> | ||||||
|  |             <DeleteTagIcon /> | ||||||
|  |         </Tooltip> | ||||||
|  |     ); | ||||||
|  |     const onDelete = canUpdateTags ? () => onTagRemove(tag) : undefined; | ||||||
|  | 
 | ||||||
|  |     if (isTagTypeColorEnabled) { | ||||||
|  |         const deleteIcon = ( | ||||||
|  |             <Tooltip title='Remove tag' arrow> | ||||||
|  |                 <ClearIcon sx={{ height: '20px', width: '20px' }} /> | ||||||
|  |             </Tooltip> | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             <Tooltip key={tagLabel} title={isOverflowing ? tagLabel : ''} arrow> | ||||||
|  |                 <span> | ||||||
|  |                     <Tag | ||||||
|  |                         tag={tag} | ||||||
|  |                         onDelete={onDelete} | ||||||
|  |                         deleteIcon={deleteIcon} | ||||||
|  |                     /> | ||||||
|  |                 </span> | ||||||
|  |             </Tooltip> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledTag | ||||||
|  |             key={tagLabel} | ||||||
|  |             label={ | ||||||
|  |                 <Tooltip | ||||||
|  |                     key={tagLabel} | ||||||
|  |                     title={isOverflowing ? tagLabel : ''} | ||||||
|  |                     arrow | ||||||
|  |                 > | ||||||
|  |                     <span> | ||||||
|  |                         {tagLabel.substring(0, 25)} | ||||||
|  |                         {isOverflowing ? ( | ||||||
|  |                             <StyledEllipsis>…</StyledEllipsis> | ||||||
|  |                         ) : ( | ||||||
|  |                             '' | ||||||
|  |                         )} | ||||||
|  |                     </span> | ||||||
|  |                 </Tooltip> | ||||||
|  |             } | ||||||
|  |             size='small' | ||||||
|  |             deleteIcon={deleteIcon} | ||||||
|  |             onDelete={onDelete} | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; | |||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| const StyledContainer = styled('div')(({ theme }) => ({ | const StyledContainer = styled('div')(({ theme }) => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -83,7 +84,7 @@ export const FeatureOverviewSidePanelTags = ({ | |||||||
|             {header} |             {header} | ||||||
|             <StyledTagContainer> |             <StyledTagContainer> | ||||||
|                 {tags.map((tag) => { |                 {tags.map((tag) => { | ||||||
|                     const tagLabel = `${tag.type}:${tag.value}`; |                     const tagLabel = formatTag(tag); | ||||||
|                     return ( |                     return ( | ||||||
|                         <StyledChip |                         <StyledChip | ||||||
|                             key={tagLabel} |                             key={tagLabel} | ||||||
|  | |||||||
| @ -14,16 +14,17 @@ import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes'; | |||||||
| import type { ITag, ITagType } from 'interfaces/tags'; | import type { ITag, ITagType } from 'interfaces/tags'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; | import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; | ||||||
|  | import type { TagSchema } from 'openapi'; | ||||||
| 
 | 
 | ||||||
| type Payload = { | type Payload = { | ||||||
|     addedTags: ITag[]; |     addedTags: TagSchema[]; | ||||||
|     removedTags: ITag[]; |     removedTags: TagSchema[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| interface IManageBulkTagsDialogProps { | interface IManageBulkTagsDialogProps { | ||||||
|     open: boolean; |     open: boolean; | ||||||
|     initialValues: ITag[]; |     initialValues: TagSchema[]; | ||||||
|     initialIndeterminateValues: ITag[]; |     initialIndeterminateValues: TagSchema[]; | ||||||
|     onCancel: () => void; |     onCancel: () => void; | ||||||
|     onSubmit: (payload: Payload) => void; |     onSubmit: (payload: Payload) => void; | ||||||
| } | } | ||||||
| @ -36,14 +37,14 @@ const StyledDialogFormContent = styled('section')(({ theme }) => ({ | |||||||
| 
 | 
 | ||||||
| const formId = 'manage-tags-form'; | const formId = 'manage-tags-form'; | ||||||
| 
 | 
 | ||||||
| const mergeTags = (tags: ITag[], newTag: ITag) => [ | const mergeTags = (tags: TagSchema[], newTag: TagSchema) => [ | ||||||
|     ...tags, |     ...tags, | ||||||
|     ...(tags.some((x) => x.value === newTag.value && x.type === newTag.type) |     ...(tags.some((x) => x.value === newTag.value && x.type === newTag.type) | ||||||
|         ? [] |         ? [] | ||||||
|         : [newTag]), |         : [newTag]), | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const filterTags = (tags: ITag[], tag: ITag) => | const filterTags = (tags: TagSchema[], tag: TagSchema) => | ||||||
|     tags.filter((x) => !(x.value === tag.value && x.type === tag.type)); |     tags.filter((x) => !(x.value === tag.value && x.type === tag.type)); | ||||||
| 
 | 
 | ||||||
| export const payloadReducer = ( | export const payloadReducer = ( | ||||||
| @ -51,11 +52,11 @@ export const payloadReducer = ( | |||||||
|     action: |     action: | ||||||
|         | { |         | { | ||||||
|               type: 'add' | 'remove'; |               type: 'add' | 'remove'; | ||||||
|               payload: ITag; |               payload: TagSchema; | ||||||
|           } |           } | ||||||
|         | { |         | { | ||||||
|               type: 'clear'; |               type: 'clear'; | ||||||
|               payload: ITag[]; |               payload: TagSchema[]; | ||||||
|           } |           } | ||||||
|         | { type: 'reset' }, |         | { type: 'reset' }, | ||||||
| ) => { | ) => { | ||||||
| @ -171,7 +172,7 @@ export const ManageBulkTagsDialog: FC<IManageBulkTagsDialogProps> = ({ | |||||||
|             value, |             value, | ||||||
|             type, |             type, | ||||||
|         }).then(async () => { |         }).then(async () => { | ||||||
|             await refetchTags(); |             refetchTags(); | ||||||
|             setSelectedTags((prev) => [...prev, { title: value }]); |             setSelectedTags((prev) => [...prev, { title: value }]); | ||||||
|             dispatch({ |             dispatch({ | ||||||
|                 type: 'add', |                 type: 'add', | ||||||
|  | |||||||
| @ -10,9 +10,10 @@ import type React from 'react'; | |||||||
| import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; | import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; | ||||||
| import CheckBoxIcon from '@mui/icons-material/CheckBox'; | import CheckBoxIcon from '@mui/icons-material/CheckBox'; | ||||||
| import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'; | 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import Add from '@mui/icons-material/Add'; | import Add from '@mui/icons-material/Add'; | ||||||
|  | import type { TagSchema } from 'openapi'; | ||||||
| 
 | 
 | ||||||
| export type TagOption = { | export type TagOption = { | ||||||
|     title: string; |     title: string; | ||||||
| @ -21,7 +22,7 @@ export type TagOption = { | |||||||
| 
 | 
 | ||||||
| interface ITagsInputProps { | interface ITagsInputProps { | ||||||
|     options: TagOption[]; |     options: TagOption[]; | ||||||
|     existingTags: ITag[]; |     existingTags: TagSchema[]; | ||||||
|     tagType: ITagType; |     tagType: ITagType; | ||||||
|     selectedOptions: TagOption[]; |     selectedOptions: TagOption[]; | ||||||
|     indeterminateOptions?: TagOption[]; |     indeterminateOptions?: TagOption[]; | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ import type { ITag } from 'interfaces/tags'; | |||||||
| import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton'; | import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton'; | ||||||
| import { useFlagLimits } from './useFlagLimits'; | import { useFlagLimits } from './useFlagLimits'; | ||||||
| import { useFeatureCreatedFeedback } from './hooks/useFeatureCreatedFeedback'; | import { useFeatureCreatedFeedback } from './hooks/useFeatureCreatedFeedback'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| interface ICreateFeatureDialogProps { | interface ICreateFeatureDialogProps { | ||||||
|     open: boolean; |     open: boolean; | ||||||
| @ -295,7 +296,7 @@ const CreateFeatureDialogContent = ({ | |||||||
|                                 description={configButtonData.tags.text} |                                 description={configButtonData.tags.text} | ||||||
|                                 selectedOptions={tags} |                                 selectedOptions={tags} | ||||||
|                                 options={allTags.map((tag) => ({ |                                 options={allTags.map((tag) => ({ | ||||||
|                                     label: `${tag.type}:${tag.value}`, |                                     label: formatTag(tag), | ||||||
|                                     value: tag, |                                     value: tag, | ||||||
|                                 }))} |                                 }))} | ||||||
|                                 onChange={setTags} |                                 onChange={setTags} | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import { | |||||||
|     type IFilterItem, |     type IFilterItem, | ||||||
| } from 'component/filter/Filters/Filters'; | } from 'component/filter/Filters/Filters'; | ||||||
| import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators'; | import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators'; | ||||||
|  | import { formatTag } from 'utils/format-tag'; | ||||||
| 
 | 
 | ||||||
| interface IProjectOverviewFilters { | interface IProjectOverviewFilters { | ||||||
|     state: FilterItemParamHolder; |     state: FilterItemParamHolder; | ||||||
| @ -23,10 +24,13 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({ | |||||||
|     const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]); |     const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const tagsOptions = (tags || []).map((tag) => ({ |         const tagsOptions = (tags || []).map((tag) => { | ||||||
|             label: `${tag.type}:${tag.value}`, |             const tagString = formatTag(tag); | ||||||
|             value: `${tag.type}:${tag.value}`, |             return { | ||||||
|         })); |                 label: tagString, | ||||||
|  |                 value: tagString, | ||||||
|  |             }; | ||||||
|  |         }); | ||||||
| 
 | 
 | ||||||
|         const flagCreatorsOptions = flagCreators.map((creator) => ({ |         const flagCreatorsOptions = flagCreators.map((creator) => ({ | ||||||
|             label: creator.name, |             label: creator.name, | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { useMemo, useState, type VFC } from 'react'; | import { useMemo, useState, type VFC } from 'react'; | ||||||
| import { Button } from '@mui/material'; | import { Button } from '@mui/material'; | ||||||
| import { ManageBulkTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog'; | 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 type { ITag } from 'interfaces/tags'; | ||||||
| import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; | import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; | ||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| @ -28,7 +28,7 @@ export const ManageTags: VFC<IManageTagsProps> = ({ | |||||||
|     const [initialValues, indeterminateValues] = useMemo(() => { |     const [initialValues, indeterminateValues] = useMemo(() => { | ||||||
|         const uniqueTags = data |         const uniqueTags = data | ||||||
|             .flatMap(({ tags }) => tags || []) |             .flatMap(({ tags }) => tags || []) | ||||||
|             .reduce<ITag[]>( |             .reduce<TagSchema[]>( | ||||||
|                 (acc, tag) => [ |                 (acc, tag) => [ | ||||||
|                     ...acc, |                     ...acc, | ||||||
|                     ...(acc.some( |                     ...(acc.some( | ||||||
| @ -56,8 +56,8 @@ export const ManageTags: VFC<IManageTagsProps> = ({ | |||||||
|         addedTags, |         addedTags, | ||||||
|         removedTags, |         removedTags, | ||||||
|     }: { |     }: { | ||||||
|         addedTags: ITag[]; |         addedTags: TagSchema[]; | ||||||
|         removedTags: ITag[]; |         removedTags: TagSchema[]; | ||||||
|     }) => { |     }) => { | ||||||
|         const features = data.map(({ name }) => name); |         const features = data.map(({ name }) => name); | ||||||
|         const payload = { features, tags: { addedTags, removedTags } }; |         const payload = { features, tags: { addedTags, removedTags } }; | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| export interface ITag { | export interface ITag { | ||||||
|     value: string; |     value: string; | ||||||
|     type: string; |     type: string; | ||||||
|  |     color?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ITagType { | export interface ITagType { | ||||||
|  | |||||||
| @ -8,6 +8,12 @@ | |||||||
|  * Representation of a [tag](https://docs.getunleash.io/reference/feature-toggles#tags)
 |  * Representation of a [tag](https://docs.getunleash.io/reference/feature-toggles#tags)
 | ||||||
|  */ |  */ | ||||||
| export interface TagSchema { | 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
 |      * The [type](https://docs.getunleash.io/reference/feature-toggles#tags) of the tag
 | ||||||
|      * @minLength 2 |      * @minLength 2 | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								frontend/src/utils/format-tag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/utils/format-tag.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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}`; | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user