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:
parent
f26bf2b8d1
commit
48b9be709e
@ -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}
|
||||
|
@ -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,
|
||||
];
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user