1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00

Chore(1-3598): new constraint edit design iteration 1 (#9727)

Implements the first step towards implementing the new design for
constraint editing. All the edit functionalities work as and when you do
them now, but there is no validation of the values you put in that's
happening.

The inverted / not inverted button and the case sensitivity button are
placeholders. They should use icons and have proper descriptions of what
they do. I'll do that in a follow-up.

The way to enter values is currently always in the section below the
main controls. Again, more work on this is coming.

Current look:

With case sensitive options:
<img width="769" alt="image"
src="https://github.com/user-attachments/assets/bfdfbac1-cc95-4f26-bf83-277bae839518"
/>

With legal values:
<img width="772" alt="image"
src="https://github.com/user-attachments/assets/14f566cc-d02a-46dd-b433-f8b13ee55bcc"
/>
This commit is contained in:
Thomas Heartman 2025-04-09 14:08:04 +02:00 committed by GitHub
parent f26bf2b8d1
commit 48b9be709e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 256 additions and 36 deletions

View File

@ -33,6 +33,7 @@ export interface IGeneralSelectProps<T extends string = string>
classes?: any;
defaultValue?: string;
visuallyHideLabel?: boolean;
variant?: 'outlined' | 'filled' | 'standard';
}
const StyledFormControl = styled(FormControl)({
@ -40,6 +41,7 @@ const StyledFormControl = styled(FormControl)({
});
function GeneralSelect<T extends string = string>({
variant = 'outlined',
name,
value,
label = '',
@ -61,7 +63,7 @@ function GeneralSelect<T extends string = string>({
return (
<StyledFormControl
variant='outlined'
variant={variant}
size='small'
classes={classes}
fullWidth={fullWidth}

View File

@ -0,0 +1,145 @@
import {
MenuItem,
InputLabel,
type SelectChangeEvent,
styled,
Select,
FormControl,
} from '@mui/material';
import {
type Operator,
stringOperators,
semVerOperators,
dateOperators,
numOperators,
inOperators,
} from 'constants/operators';
import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription';
import { useId } from 'react';
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
interface IConstraintOperatorSelectProps {
options: Operator[];
value: Operator;
onChange: (value: Operator) => void;
inverted?: boolean;
}
const StyledSelect = styled(Select)(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.25, 0),
fontSize: theme.fontSizes.smallerBody,
background: theme.palette.secondary.light,
border: `1px solid ${theme.palette.secondary.border}`,
color: theme.palette.secondary.dark,
fontWeight: theme.typography.fontWeightBold,
fieldset: {
border: 'none',
},
transition: 'all 0.03s ease',
'&:is(:hover, :focus-within)': {
outline: `1px solid ${theme.palette.primary.main}`,
},
'&::before,&::after': {
border: 'none',
},
'.MuiInput-input': {
paddingBlock: theme.spacing(0.25),
},
}));
const StyledMenuItem = styled(MenuItem, {
shouldForwardProp: (prop) => prop !== 'separator',
})<{ separator: boolean }>(({ theme, separator }) =>
separator
? {
position: 'relative',
overflow: 'visible',
marginTop: theme.spacing(2),
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: theme.spacing(-1),
left: 0,
right: 0,
borderTop: '1px solid',
borderTopColor: theme.palette.divider,
},
}
: {},
);
const StyledValue = styled('span')(({ theme }) => ({
paddingInline: theme.spacing(1),
}));
export const ConstraintOperatorSelect = ({
options,
value,
onChange,
inverted,
}: IConstraintOperatorSelectProps) => {
const selectId = useId();
const labelId = useId();
const onSelectChange = (event: SelectChangeEvent<unknown>) => {
onChange(event.target.value as Operator);
};
const renderValue = () => {
return (
<StyledValue>
{formatOperatorDescription(value, inverted)}
</StyledValue>
);
};
return (
<FormControl variant='standard' size='small' hiddenLabel>
<ScreenReaderOnly>
<InputLabel id={labelId} htmlFor={selectId}>
Operator
</InputLabel>
</ScreenReaderOnly>
<StyledSelect
id={selectId}
labelId={labelId}
name='operator'
disableUnderline
value={value}
onChange={onSelectChange}
renderValue={renderValue}
IconComponent={KeyboardArrowDownOutlined}
>
{options.map((operator) => (
<StyledMenuItem
key={operator}
value={operator}
separator={needSeparatorAbove(options, operator)}
>
{formatOperatorDescription(operator, inverted)}
</StyledMenuItem>
))}
</StyledSelect>
</FormControl>
);
};
const needSeparatorAbove = (options: Operator[], option: Operator): boolean => {
if (option === options[0]) {
return false;
}
return operatorGroups.some((group) => {
return group[0] === option;
});
};
const operatorGroups = [
inOperators,
stringOperators,
numOperators,
dateOperators,
semVerOperators,
];

View File

@ -1,4 +1,4 @@
import { styled } from '@mui/material';
import { IconButton, styled } from '@mui/material';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { DateSingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue';
import { FreeTextInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/FreeTextInput/FreeTextInput';
@ -17,8 +17,6 @@ import {
STRING_OPERATORS_LEGAL_VALUES,
type Input,
} from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
import { CaseSensitiveButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton';
import { ConstraintOperatorSelect } from 'component/common/NewConstraintAccordion/ConstraintOperatorSelect';
import {
DATE_AFTER,
dateOperators,
@ -38,14 +36,26 @@ import {
CURRENT_TIME_CONTEXT_FIELD,
operatorsForContext,
} from 'utils/operatorsForContext';
import { ConstraintOperatorSelect } from './ConstraintOperatorSelect';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import Delete from '@mui/icons-material/Delete';
const Container = styled('article')(({ theme }) => ({
'--padding': theme.spacing(2),
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusLarge,
border: `1px solid ${theme.palette.divider}`,
}));
const TopRow = styled('div')(({ theme }) => ({
padding: 'var(--padding)',
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
justifyItems: 'space-between',
borderBottom: `1px dashed ${theme.palette.divider}`,
}));
const resolveLegalValues = (
values: IConstraint['values'],
legalValues: IUnleashContextDefinition['legalValues'],
@ -72,6 +82,46 @@ const resolveLegalValues = (
};
};
const ConstraintDetails = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
flexFlow: 'row nowrap',
width: '100%',
height: 'min-content',
}));
const InputContainer = styled('div')(({ theme }) => ({
padding: 'var(--padding)',
paddingTop: 0,
}));
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
fieldset: { border: 'none', borderRadius: 0 },
':focus-within fieldset': { borderBottomStyle: 'solid' },
'label + &': {
// mui adds a margin top to 'standard' selects with labels
margin: 0,
},
'&::before': {
border: 'none',
},
}));
const StyledButton = styled('button')(({ theme }) => ({
width: '5ch',
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.25, 0),
fontSize: theme.fontSizes.smallerBody,
background: theme.palette.secondary.light,
border: `1px solid ${theme.palette.secondary.border}`,
color: theme.palette.secondary.dark,
fontWeight: theme.typography.fontWeightBold,
transition: 'all 0.03s ease',
'&:is(:hover, :focus-visible)': {
outline: `1px solid ${theme.palette.primary.main}`,
},
}));
type Props = {
localConstraint: IConstraint;
setContextName: (contextName: string) => void;
@ -79,8 +129,8 @@ type Props = {
setLocalConstraint: React.Dispatch<React.SetStateAction<IConstraint>>;
action: string;
onDelete?: () => void;
setInvertedOperator: () => void;
setCaseInsensitive: () => void;
toggleInvertedOperator: () => void;
toggleCaseSensitivity: () => void;
onUndo: () => void;
constraintChanges: IConstraint[];
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
@ -102,8 +152,8 @@ export const EditableConstraint: FC<Props> = ({
setOperator,
onDelete,
onUndo,
setInvertedOperator,
setCaseInsensitive,
toggleInvertedOperator,
toggleCaseSensitivity,
input,
contextDefinition,
constraintValues,
@ -175,7 +225,7 @@ export const EditableConstraint: FC<Props> = ({
}
};
const resolveInput = () => {
const Input = () => {
switch (input) {
case IN_OPERATORS_LEGAL_VALUES:
case STRING_OPERATORS_LEGAL_VALUES:
@ -291,31 +341,43 @@ export const EditableConstraint: FC<Props> = ({
return (
<Container>
<GeneralSelect
id='context-field-select'
name='contextName'
label='Context Field'
autoFocus
options={constraintNameOptions}
value={contextName || ''}
onChange={setContextName}
/>
<ConstraintOperatorSelect
options={operatorsForContext(contextName)}
value={operator}
onChange={onOperatorChange}
/>
<TopRow>
<ConstraintDetails>
<StyledSelect
visuallyHideLabel
id='context-field-select'
name='contextName'
label='Context Field'
autoFocus
options={constraintNameOptions}
value={contextName || ''}
onChange={setContextName}
variant='standard'
/>
{/* this is how to style them */}
{/* <StrategyEvaluationChip label='label' /> */}
{showCaseSensitiveButton ? (
<CaseSensitiveButton
localConstraint={localConstraint}
setCaseInsensitive={setCaseInsensitive}
/>
) : null}
{resolveInput()}
{/* <ul>
<StyledButton
type='button'
onClick={toggleInvertedOperator}
>
{localConstraint.inverted ? 'aint' : 'is'}
</StyledButton>
<ConstraintOperatorSelect
options={operatorsForContext(contextName)}
value={operator}
onChange={onOperatorChange}
inverted={localConstraint.inverted}
/>
{showCaseSensitiveButton ? (
<StyledButton
type='button'
onClick={toggleCaseSensitivity}
>
{localConstraint.caseInsensitive ? 'Aa' : 'A/a'}
</StyledButton>
) : null}
{/* <ul>
<li>
<Chip
label='value1'
@ -329,6 +391,17 @@ export const EditableConstraint: FC<Props> = ({
/>
</li>
</ul> */}
</ConstraintDetails>
<HtmlTooltip title='Delete constraint' arrow>
<IconButton type='button' size='small' onClick={onDelete}>
<Delete />
</IconButton>
</HtmlTooltip>
</TopRow>
<InputContainer>
<Input />
</InputContainer>
</Container>
);
};

View File

@ -191,8 +191,8 @@ export const EditableConstraintWrapper = ({
setContextName={setContextName}
setOperator={setOperator}
action={action}
setInvertedOperator={setInvertedOperator}
setCaseInsensitive={setCaseInsensitive}
toggleInvertedOperator={setInvertedOperator}
toggleCaseSensitivity={setCaseInsensitive}
onDelete={onDelete}
onUndo={onUndo}
constraintChanges={constraintChanges}