1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-17 13:46:47 +02:00

feat: improve constraints item on small screens (#9609)

Fixing constraint operator item, items alignment and padding for better presentation on mobile devices.
This commit is contained in:
Tymoteusz Czech 2025-03-27 13:33:25 +01:00 committed by GitHub
parent f7c04cc2cb
commit cf053470e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 132 additions and 59 deletions

View File

@ -9,7 +9,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import type { IConstraint } from 'interfaces/strategy'; import type { IConstraint } from 'interfaces/strategy';
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody'; import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader'; import { ConstraintAccordionViewHeader } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
import { oneOf } from 'utils/oneOf'; import { oneOf } from 'utils/oneOf';
import { import {
dateOperators, dateOperators,

View File

@ -19,6 +19,7 @@ const Operator: FC<{
<Tooltip title={inverted ? `Not ${label}` : label} arrow> <Tooltip title={inverted ? `Not ${label}` : label} arrow>
<StrategyEvaluationChip <StrategyEvaluationChip
label={formatOperatorDescription(label, inverted)} label={formatOperatorDescription(label, inverted)}
multiline
/> />
</Tooltip> </Tooltip>
); );
@ -49,10 +50,15 @@ const CaseSensitive: FC = () => {
}; };
const StyledConstraintContainer = styled('div')(({ theme }) => ({ const StyledConstraintContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
[theme.breakpoints.up('sm')]: {
gap: theme.spacing(2),
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(3, auto)', gridTemplateColumns: 'repeat(3, auto)',
gap: theme.spacing(2),
placeItems: 'center', placeItems: 'center',
},
})); }));
const StyledOperatorGroup = styled('div')(({ theme }) => ({ const StyledOperatorGroup = styled('div')(({ theme }) => ({
@ -65,6 +71,9 @@ const StyledConstraintName = styled('div')(({ theme }) => ({
maxWidth: '150px', maxWidth: '150px',
paddingRight: theme.spacing(0.5), paddingRight: theme.spacing(0.5),
overflow: 'hidden', overflow: 'hidden',
[theme.breakpoints.down('sm')]: {
maxWidth: 'unset',
},
})); }));
type ConstraintItemHeaderProps = ConstraintSchema & { type ConstraintItemHeaderProps = ConstraintSchema & {

View File

@ -0,0 +1,20 @@
import { Box, styled } from '@mui/material';
export const ConstraintSeparator = styled(({ ...props }) => (
<Box role='separator' {...props}>
and
</Box>
))(({ theme }) => ({
position: 'absolute',
top: theme.spacing(-0.5),
left: theme.spacing(2),
transform: 'translateY(-50%)',
padding: theme.spacing(0.75, 1),
lineHeight: 1,
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.primary,
background: theme.palette.background.application,
borderRadius: theme.shape.borderRadiusLarge,
zIndex: theme.zIndex.fab,
textTransform: 'uppercase',
}));

View File

@ -1,5 +1,6 @@
import { Children, isValidElement, type FC, type ReactNode } from 'react'; import { Children, isValidElement, type FC, type ReactNode } from 'react';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { ConstraintSeparator } from './ConstraintSeparator/ConstraintSeparator';
const StyledList = styled('ul')(({ theme }) => ({ const StyledList = styled('ul')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -15,7 +16,7 @@ export const ConstraintListItem = styled('div')(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
background: theme.palette.background.default, background: theme.palette.background.default,
padding: theme.spacing(1.5, 3), padding: theme.spacing(1.5, 2),
display: 'flex', display: 'flex',
flexFlow: 'column', flexFlow: 'column',
gap: theme.spacing(1), gap: theme.spacing(1),
@ -25,20 +26,6 @@ const StyledListItem = styled('li')({
position: 'relative', position: 'relative',
}); });
const StyledAnd = styled('div')(({ theme }) => ({
position: 'absolute',
top: theme.spacing(-0.5),
left: theme.spacing(2),
transform: 'translateY(-50%)',
padding: theme.spacing(0.75, 1),
lineHeight: 1,
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.primary,
background: theme.palette.background.application,
borderRadius: theme.shape.borderRadiusLarge,
zIndex: theme.zIndex.fab,
}));
export const ConstraintsList: FC<{ children: ReactNode }> = ({ children }) => { export const ConstraintsList: FC<{ children: ReactNode }> = ({ children }) => {
const result: ReactNode[] = []; const result: ReactNode[] = [];
Children.forEach(children, (child, index) => { Children.forEach(children, (child, index) => {
@ -46,9 +33,7 @@ export const ConstraintsList: FC<{ children: ReactNode }> = ({ children }) => {
result.push( result.push(
<StyledListItem key={index}> <StyledListItem key={index}>
{index > 0 ? ( {index > 0 ? (
<StyledAnd role='separator' key={`${index}-divider`}> <ConstraintSeparator key={`${index}-divider`} />
AND
</StyledAnd>
) : null} ) : null}
{child} {child}
</StyledListItem>, </StyledListItem>,

View File

@ -2,7 +2,8 @@ import { forwardRef } from 'react';
import type { ChipProps } from '@mui/material'; import type { ChipProps } from '@mui/material';
import { Chip, styled } from '@mui/material'; import { Chip, styled } from '@mui/material';
const StyledChip = styled(Chip)(({ theme }) => ({ const StyledChip = styled(Chip)<{ multiline?: boolean }>(
({ theme, multiline }) => ({
borderRadius: `${theme.shape.borderRadius}px`, borderRadius: `${theme.shape.borderRadius}px`,
padding: theme.spacing(0.25, 0), padding: theme.spacing(0.25, 0),
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
@ -11,8 +12,17 @@ const StyledChip = styled(Chip)(({ theme }) => ({
border: `1px solid ${theme.palette.secondary.border}`, border: `1px solid ${theme.palette.secondary.border}`,
color: theme.palette.secondary.dark, color: theme.palette.secondary.dark,
fontWeight: theme.typography.fontWeightBold, fontWeight: theme.typography.fontWeightBold,
})); ...(multiline
? {
export const StrategyEvaluationChip = forwardRef<HTMLDivElement, ChipProps>( '.MuiChip-label': {
(props, ref) => <StyledChip size='small' ref={ref} {...props} />, whiteSpace: 'collapse',
},
}
: {}),
}),
); );
export const StrategyEvaluationChip = forwardRef<
HTMLDivElement,
ChipProps & { multiline?: boolean }
>((props, ref) => <StyledChip size='small' ref={ref} {...props} />);

View File

@ -13,6 +13,9 @@ const StyledContainer = styled('div')(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
fontSize: theme.typography.body2.fontSize, fontSize: theme.typography.body2.fontSize,
minHeight: theme.spacing(4), minHeight: theme.spacing(4),
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
})); }));
const StyledContent = styled('div')(({ theme }) => ({ const StyledContent = styled('div')(({ theme }) => ({
@ -23,6 +26,9 @@ const StyledContent = styled('div')(({ theme }) => ({
filter: 'grayscale(1)', filter: 'grayscale(1)',
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
}, },
[theme.breakpoints.down('sm')]: {
width: '100%',
},
})); }));
const StyledType = styled('span')(({ theme }) => ({ const StyledType = styled('span')(({ theme }) => ({
@ -32,7 +38,11 @@ const StyledType = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold, fontWeight: theme.typography.fontWeightBold,
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
width: theme.spacing(10), width: theme.spacing(10),
[theme.breakpoints.down('sm')]: {
width: '100%',
},
})); }));
/** /**
* Abstract building block for a list of constraints, segments and other items inside a strategy * Abstract building block for a list of constraints, segments and other items inside a strategy
*/ */

View File

@ -11,9 +11,13 @@ export type ValuesListProps = {
tooltips?: Record<string, string | undefined>; tooltips?: Record<string, string | undefined>;
} & Pick<TruncatorProps, 'onSetTruncated'>; } & Pick<TruncatorProps, 'onSetTruncated'>;
const StyledValuesContainer = styled('div')({ const StyledValuesContainer = styled('div')(({ theme }) => ({
flex: '1 1 0', flex: '1 1 0',
}); [theme.breakpoints.down('sm')]: {
display: 'block',
float: 'left',
},
}));
const StyledTruncator = styled(Truncator)({ const StyledTruncator = styled(Truncator)({
padding: 0, padding: 0,

View File

@ -13,17 +13,15 @@ const StyledHeaderWrapper = styled('div')(({ theme }) => ({
const StyledHeaderMetaInfo = styled('div')(({ theme }) => ({ const StyledHeaderMetaInfo = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'stretch', alignItems: 'stretch',
marginLeft: theme.spacing(1),
[theme.breakpoints.down('sm')]: { [theme.breakpoints.down('sm')]: {
marginLeft: 0, marginLeft: 0,
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center',
width: '100%', width: '100%',
}, },
})); }));
const StyledExpandItem = styled('div')(({ theme }) => ({ const StyledExpandItem = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary, color: theme.palette.secondary.main,
margin: theme.spacing(0.25, 0, 0, 0.75), margin: theme.spacing(0.25, 0, 0, 0.75),
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
})); }));

View File

@ -12,6 +12,8 @@ import {
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion'; import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion';
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
import { useUiFlag } from 'hooks/useUiFlag';
export interface IConstraintAccordionListProps { export interface IConstraintAccordionListProps {
constraints: IConstraint[]; constraints: IConstraint[];
@ -83,6 +85,7 @@ export const NewConstraintAccordionList = forwardRef<
IConstraintList IConstraintList
>(({ constraints, setConstraints, state }, ref) => { >(({ constraints, setConstraints, state }, ref) => {
const { context } = useUnleashContext(); const { context } = useUnleashContext();
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
const onEdit = const onEdit =
setConstraints && setConstraints &&
@ -139,6 +142,28 @@ export const NewConstraintAccordionList = forwardRef<
return null; return null;
} }
if (flagOverviewRedesign) {
return (
<StyledContainer id={constraintAccordionListId}>
<ConstraintsList>
{constraints.map((constraint, index) => (
<NewConstraintAccordion
key={constraint[constraintId]}
constraint={constraint}
onEdit={onEdit?.bind(null, constraint)}
onCancel={onCancel.bind(null, index)}
onDelete={onRemove?.bind(null, index)}
onSave={onSave?.bind(null, index)}
onAutoSave={onAutoSave?.(constraint[constraintId])}
editing={Boolean(state.get(constraint)?.editing)}
compact
/>
))}
</ConstraintsList>
</StyledContainer>
);
}
return ( return (
<StyledContainer id={constraintAccordionListId}> <StyledContainer id={constraintAccordionListId}>
{constraints.map((constraint, index) => { {constraints.map((constraint, index) => {

View File

@ -9,12 +9,12 @@ import {
styled, styled,
} from '@mui/material'; } from '@mui/material';
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem'; import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
import { ConstraintItemHeader } from 'component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { import {
ConstraintListItem, ConstraintListItem,
ConstraintsList, ConstraintsList,
} from 'component/common/ConstraintsList/ConstraintsList'; } from 'component/common/ConstraintsList/ConstraintsList';
import { ConstraintAccordionViewHeaderInfo } from '../NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo';
type SegmentItemProps = { type SegmentItemProps = {
segment: Partial<ISegment>; segment: Partial<ISegment>;
@ -39,7 +39,6 @@ const StyledAccordion = styled(Accordion)(() => ({
})); }));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
padding: theme.spacing(0, 3),
fontSize: theme.typography.body2.fontSize, fontSize: theme.typography.body2.fontSize,
minHeight: 'unset', minHeight: 'unset',
})); }));
@ -48,12 +47,13 @@ const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
padding: theme.spacing(0.5, 3, 2.5), padding: theme.spacing(0.5, 3, 2.5),
})); }));
const StyledLink = styled(Link)({ const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none', textDecoration: 'none',
paddingRight: theme.spacing(0.5),
'&:hover': { '&:hover': {
textDecoration: 'underline', textDecoration: 'underline',
}, },
}); }));
const StyledActionsContainer = styled('div')(({ theme }) => ({ const StyledActionsContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -90,8 +90,13 @@ export const SegmentItem: FC<SegmentItemProps> = ({
<ConstraintListItem <ConstraintListItem
key={`${objectId(constraint)}-${index}`} key={`${objectId(constraint)}-${index}`}
> >
{/* FIXME: use accordion */} <ConstraintAccordionViewHeaderInfo
<ConstraintItemHeader {...constraint} /> constraint={constraint}
expanded={isOpen}
allowExpand={(shouldExpand) =>
setIsOpen(shouldExpand)
}
/>
</ConstraintListItem> </ConstraintListItem>
))} ))}
</ConstraintsList> </ConstraintsList>
@ -107,7 +112,11 @@ export const SegmentItem: FC<SegmentItemProps> = ({
return ( return (
<StyledConstraintListItem> <StyledConstraintListItem>
<StyledAccordion expanded={isOpen} disableGutters> <StyledAccordion
expanded={isOpen}
disableGutters
TransitionProps={{ mountOnEnter: true, unmountOnExit: true }}
>
<StyledAccordionSummary id={`segment-accordion-${segment.id}`}> <StyledAccordionSummary id={`segment-accordion-${segment.id}`}>
<StrategyEvaluationItem type='Segment'> <StrategyEvaluationItem type='Segment'>
<StyledLink to={`/segments/edit/${segment.id}`}> <StyledLink to={`/segments/edit/${segment.id}`}>

View File

@ -28,8 +28,9 @@ export const RolloutParameter: FC<{
return ( return (
<> <>
<StrategyEvaluationItem type='Rollout %'> <StrategyEvaluationItem type='Rollout %'>
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '} <StrategyEvaluationChip label={`${percentage}%`} />
{stickiness} <p>
of your base {stickiness}{' '}
<span> <span>
{hasConstraints ? 'who match constraints ' : ' '} {hasConstraints ? 'who match constraints ' : ' '}
is included. is included.
@ -37,8 +38,10 @@ export const RolloutParameter: FC<{
{displayGroupId && parameters?.groupId ? ( {displayGroupId && parameters?.groupId ? (
<StrategyEvaluationChip <StrategyEvaluationChip
label={`groupId: ${parameters?.groupId}`} label={`groupId: ${parameters?.groupId}`}
component='span'
/> />
) : null} ) : null}
</p>
</StrategyEvaluationItem> </StrategyEvaluationItem>
<RolloutVariants variants={variants} /> <RolloutVariants variants={variants} />
</> </>