1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

Feat/tag type colors frontend (#9566)

Add frontend support for tag type colors
This commit is contained in:
Fredrik Strand Oseberg 2025-03-19 10:03:33 +01:00 committed by GitHub
parent df351808c1
commit aad5a6a1a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 329 additions and 112 deletions

View File

@ -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}
>
<CreateButton name='type' permission={CREATE_TAG_TYPE} />
</TagTypeForm>

View File

@ -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}
>
<UpdateButton permission={UPDATE_TAG_TYPE} />
</TagForm>

View File

@ -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<ITagTypeColorPickerProps> = ({
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 (
<StyledColorContainer>
<StyledColorsWrapper>
{colorOptions.map((color) => (
<Box
key={color.value}
title={color.name}
sx={{ display: 'flex', alignItems: 'center' }}
>
<StyledColorCircle
$color={color.value}
$selected={selectedColor === color.value}
onClick={() => onChange(color.value)}
type='button'
aria-label={`Select ${color.name} color`}
/>
</Box>
))}
</StyledColorsWrapper>
</StyledColorContainer>
);
};

View File

@ -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<React.SetStateAction<string>>;
setTagDesc: React.Dispatch<React.SetStateAction<string>>;
setColor: React.Dispatch<React.SetStateAction<string>>;
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<ITagTypeForm> = ({
handleCancel,
tagName,
tagDesc,
color,
setTagName,
setTagDesc,
setColor,
errors,
mode,
validateNameUniqueness,
clearErrors,
isTagTypeColorEnabled,
}) => {
return (
<StyledForm onSubmit={handleSubmit}>
@ -95,6 +102,20 @@ const TagTypeForm: React.FC<ITagTypeForm> = ({
value={tagDesc}
onChange={(e) => setTagDesc(e.target.value)}
/>
<ConditionallyRender
condition={isTagTypeColorEnabled}
show={
<>
<Typography variant='body2'>
Tag color
<TagTypeColorPicker
selectedColor={color}
onChange={setColor}
/>
</Typography>
</>
}
/>
</StyledContainer>
<StyledButtonContainer>
{children}

View File

@ -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,
};
};

View File

@ -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 (
<LinkCell
data-loading
title={name}
subtitle={description}
/>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ConditionallyRender
condition={
isTagTypeColorEnabled && Boolean(color)
}
show={<StyledColorDot $color={color} />}
/>
<LinkCell
data-loading
title={name}
subtitle={description}
/>
</Box>
);
},
sortType: 'alphanumeric',
@ -136,7 +162,7 @@ export const TagTypeList = () => {
disableSortBy: true,
},
],
[navigate],
[navigate, isTagTypeColorEnabled],
);
const initialState = useMemo(

View File

@ -266,29 +266,33 @@ exports[`renders an empty list correctly 1`] = `
role="cell"
>
<div
className="css-1prhjm6"
className="MuiBox-root css-70qvj9"
>
<div
className="css-u8cmsa"
className="css-1prhjm6"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
<div
className="css-u8cmsa"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
}
}
}
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
</div>
</div>
</div>
</td>
@ -436,29 +440,33 @@ exports[`renders an empty list correctly 1`] = `
role="cell"
>
<div
className="css-1prhjm6"
className="MuiBox-root css-70qvj9"
>
<div
className="css-u8cmsa"
className="css-1prhjm6"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
<div
className="css-u8cmsa"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
}
}
}
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
</div>
</div>
</div>
</td>
@ -606,29 +614,33 @@ exports[`renders an empty list correctly 1`] = `
role="cell"
>
<div
className="css-1prhjm6"
className="MuiBox-root css-70qvj9"
>
<div
className="css-u8cmsa"
className="css-1prhjm6"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
<div
className="css-u8cmsa"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
}
}
}
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
</div>
</div>
</div>
</td>
@ -776,29 +788,33 @@ exports[`renders an empty list correctly 1`] = `
role="cell"
>
<div
className="css-1prhjm6"
className="MuiBox-root css-70qvj9"
>
<div
className="css-u8cmsa"
className="css-1prhjm6"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
<div
className="css-u8cmsa"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
}
}
}
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
</div>
</div>
</div>
</td>
@ -946,29 +962,33 @@ exports[`renders an empty list correctly 1`] = `
role="cell"
>
<div
className="css-1prhjm6"
className="MuiBox-root css-70qvj9"
>
<div
className="css-u8cmsa"
className="css-1prhjm6"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
<div
className="css-u8cmsa"
>
<span
className="css-697v50"
data-loading={true}
style={
{
"WebkitLineClamp": 1,
"lineClamp": 1,
}
}
}
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
>
Tag type name
</span>
<span
className="css-1121jr7"
data-loading={true}
>
Tag type description when loading
</span>
</div>
</div>
</div>
</td>

View File

@ -7,9 +7,11 @@ export interface ITagType {
name: string;
description: string;
icon: string;
color?: string;
}
export interface ITagPayload {
name: string;
description: string;
color?: string;
}

View File

@ -92,6 +92,7 @@ export type UiFlags = {
consumptionModel?: boolean;
edgeObservability?: boolean;
adminNavUI?: boolean;
tagTypeColor?: boolean;
};
export interface IVersionInfo {

View File

@ -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 = {

View File

@ -59,6 +59,7 @@ process.nextTick(async () => {
teamsIntegrationChangeRequests: true,
simplifyDisableFeature: true,
adminNavUI: false,
tagTypeColor: true,
},
},
authentication: {