mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02: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 { 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 (
|
||||
<ConditionallyRender
|
||||
condition={tag.length > 30}
|
||||
show={<HtmlTooltip title={tag}>{children}</HtmlTooltip>}
|
||||
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<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 (
|
||||
<HtmlTooltip
|
||||
title={tags.map((tag) => (
|
||||
<Box
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => onClick(tag)}
|
||||
key={tag}
|
||||
>
|
||||
{tag}
|
||||
</Box>
|
||||
))}
|
||||
title={tags.map((tag) => {
|
||||
const formattedTag = formatTag(tag);
|
||||
return (
|
||||
<Box
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => onClick(formattedTag)}
|
||||
key={formattedTag}
|
||||
>
|
||||
{isTagTypeColorEnabled ? (
|
||||
<Tag tag={tag} maxLength={30} />
|
||||
) : (
|
||||
formattedTag
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
>
|
||||
<Tag sx={{ cursor: 'initial' }}>{tags.length} more...</Tag>
|
||||
<CustomTagButton
|
||||
sx={{
|
||||
cursor: 'initial',
|
||||
...(isTagTypeColorEnabled && {
|
||||
borderRadius: (theme) => theme.spacing(2),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{tags.length} more...
|
||||
</CustomTagButton>
|
||||
</HtmlTooltip>
|
||||
);
|
||||
};
|
||||
@ -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 (
|
||||
<TagsContainer>
|
||||
{tag1 && (
|
||||
<CappedTag tag={tag1}>
|
||||
<Tag onClick={() => onClick(tag1)}>{tag1}</Tag>
|
||||
</CappedTag>
|
||||
)}
|
||||
{tag2 && (
|
||||
<CappedTag tag={tag2}>
|
||||
<Tag onClick={() => onClick(tag2)}>{tag2}</Tag>
|
||||
</CappedTag>
|
||||
)}
|
||||
{tag3 && (
|
||||
<CappedTag tag={tag3}>
|
||||
<Tag onClick={() => onClick(tag3)}>{tag3}</Tag>
|
||||
</CappedTag>
|
||||
)}
|
||||
{tag1 && <TagItem tag={tag1} onClick={handleTagClick} />}
|
||||
{tag2 && <TagItem tag={tag2} onClick={handleTagClick} />}
|
||||
{tag3 && <TagItem tag={tag3} onClick={handleTagClick} />}
|
||||
<ConditionallyRender
|
||||
condition={restTags.length > 0}
|
||||
show={<RestTags tags={restTags} onClick={onClick} />}
|
||||
|
@ -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<IFeatureTagCellProps> = ({ row }) => {
|
||||
return <TextCell />;
|
||||
|
||||
const value =
|
||||
row.original.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '';
|
||||
row.original.tags?.map((tag: TagSchema) => formatTag(tag)).join('\n') ||
|
||||
'';
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
@ -39,7 +39,7 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row }) => {
|
||||
{row.original.tags?.map((tag) => (
|
||||
<StyledTag key={tag.type + tag.value}>
|
||||
<Highlighter search={searchQuery}>
|
||||
{`${tag.type}:${tag.value}`}
|
||||
{formatTag(tag)}
|
||||
</Highlighter>
|
||||
</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,
|
||||
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<IFeatureToggleFiltersProps> = ({
|
||||
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;
|
||||
|
@ -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<ITag>();
|
||||
@ -103,49 +106,17 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
|
||||
<StyledTagRow>
|
||||
<StyledLabel>Tags:</StyledLabel>
|
||||
<StyledTagContainer>
|
||||
{tags.map((tag) => {
|
||||
const tagLabel = `${tag.type}:${tag.value}`;
|
||||
const isOverflowing = tagLabel.length > 25;
|
||||
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={
|
||||
<Tooltip title='Remove tag' arrow>
|
||||
<DeleteTagIcon />
|
||||
</Tooltip>
|
||||
}
|
||||
onDelete={
|
||||
canUpdateTags
|
||||
? () => {
|
||||
setRemoveTagOpen(true);
|
||||
setSelectedTag(tag);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{tags.map((tag) => (
|
||||
<TagItem
|
||||
key={formatTag(tag)}
|
||||
tag={tag}
|
||||
canUpdateTags={canUpdateTags}
|
||||
onTagRemove={(tag) => {
|
||||
setRemoveTagOpen(true);
|
||||
setSelectedTag(tag);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{canUpdateTags ? (
|
||||
<AddTagButton
|
||||
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 { 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}
|
||||
<StyledTagContainer>
|
||||
{tags.map((tag) => {
|
||||
const tagLabel = `${tag.type}:${tag.value}`;
|
||||
const tagLabel = formatTag(tag);
|
||||
return (
|
||||
<StyledChip
|
||||
key={tagLabel}
|
||||
|
@ -14,16 +14,17 @@ import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
|
||||
import type { ITag, ITagType } from 'interfaces/tags';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import useTagApi from 'hooks/api/actions/useTagApi/useTagApi';
|
||||
import type { TagSchema } from 'openapi';
|
||||
|
||||
type Payload = {
|
||||
addedTags: ITag[];
|
||||
removedTags: ITag[];
|
||||
addedTags: TagSchema[];
|
||||
removedTags: TagSchema[];
|
||||
};
|
||||
|
||||
interface IManageBulkTagsDialogProps {
|
||||
open: boolean;
|
||||
initialValues: ITag[];
|
||||
initialIndeterminateValues: ITag[];
|
||||
initialValues: TagSchema[];
|
||||
initialIndeterminateValues: TagSchema[];
|
||||
onCancel: () => 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<IManageBulkTagsDialogProps> = ({
|
||||
value,
|
||||
type,
|
||||
}).then(async () => {
|
||||
await refetchTags();
|
||||
refetchTags();
|
||||
setSelectedTags((prev) => [...prev, { title: value }]);
|
||||
dispatch({
|
||||
type: 'add',
|
||||
|
@ -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[];
|
||||
|
@ -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}
|
||||
|
@ -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<IProjectOverviewFilters> = ({
|
||||
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
|
||||
|
||||
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,
|
||||
|
@ -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<IManageTagsProps> = ({
|
||||
const [initialValues, indeterminateValues] = useMemo(() => {
|
||||
const uniqueTags = data
|
||||
.flatMap(({ tags }) => tags || [])
|
||||
.reduce<ITag[]>(
|
||||
.reduce<TagSchema[]>(
|
||||
(acc, tag) => [
|
||||
...acc,
|
||||
...(acc.some(
|
||||
@ -56,8 +56,8 @@ export const ManageTags: VFC<IManageTagsProps> = ({
|
||||
addedTags,
|
||||
removedTags,
|
||||
}: {
|
||||
addedTags: ITag[];
|
||||
removedTags: ITag[];
|
||||
addedTags: TagSchema[];
|
||||
removedTags: TagSchema[];
|
||||
}) => {
|
||||
const features = data.map(({ name }) => name);
|
||||
const payload = { features, tags: { addedTags, removedTags } };
|
||||
|
@ -1,6 +1,7 @@
|
||||
export interface ITag {
|
||||
value: string;
|
||||
type: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface ITagType {
|
||||
|
@ -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
|
||||
|
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