1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

chore: address UX feedback for add strategy modal (#10698)

https://linear.app/unleash/issue/2-3911/address-new-ux-feedback

Addresses UX feedback regarding new "add strategy" modal:
- "View more strategies" instead of "View more"
- Avoid horizontal scroll
- Fix responsiveness
- Prevent flicker when navigating between strategy and preview modals
This commit is contained in:
Nuno Góis 2025-09-26 14:54:08 +01:00 committed by GitHub
parent 99c4f7111a
commit b865ee44f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 172 deletions

View File

@ -1,14 +1,15 @@
import type React from 'react'; import type React from 'react';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import PermissionButton, { import PermissionButton, {
type IPermissionButtonProps, type IPermissionButtonProps,
} from 'component/common/PermissionButton/PermissionButton'; } from 'component/common/PermissionButton/PermissionButton';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { Dialog, styled } from '@mui/material'; import { Box, Dialog, IconButton, styled, Typography } from '@mui/material';
import { LegacyFeatureStrategyMenuCards } from './LegacyFeatureStrategyMenuCards/LegacyFeatureStrategyMenuCards.tsx'; import { LegacyFeatureStrategyMenuCards } from './LegacyFeatureStrategyMenuCards/LegacyFeatureStrategyMenuCards.tsx';
import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate.tsx'; import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate.tsx';
import MoreVert from '@mui/icons-material/MoreVert'; import MoreVert from '@mui/icons-material/MoreVert';
import CloseIcon from '@mui/icons-material/Close';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
@ -20,7 +21,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { LegacyReleasePlanReviewDialog } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/LegacyReleasePlanReviewDialog.tsx'; import { LegacyReleasePlanReviewDialog } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/LegacyReleasePlanReviewDialog.tsx';
import { ReleasePlanReviewDialog } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlanReviewDialog.tsx'; import { ReleasePlanPreview } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx';
import { import {
FeatureStrategyMenuCards, FeatureStrategyMenuCards,
type StrategyFilterValue, type StrategyFilterValue,
@ -34,7 +35,6 @@ interface IFeatureStrategyMenuProps {
environmentId: string; environmentId: string;
variant?: IPermissionButtonProps['variant']; variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean; matchWidth?: boolean;
size?: IPermissionButtonProps['size'];
disableReason?: string; disableReason?: string;
} }
@ -52,13 +52,19 @@ const StyledAdditionalMenuButton = styled(PermissionButton)(({ theme }) => ({
paddingBlock: 0, paddingBlock: 0,
})); }));
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4),
}));
export const FeatureStrategyMenu = ({ export const FeatureStrategyMenu = ({
label, label,
projectId, projectId,
featureId, featureId,
environmentId, environmentId,
variant, variant,
size,
matchWidth, matchWidth,
disableReason, disableReason,
}: IFeatureStrategyMenuProps) => { }: IFeatureStrategyMenuProps) => {
@ -71,6 +77,7 @@ export const FeatureStrategyMenu = ({
const [selectedTemplate, setSelectedTemplate] = const [selectedTemplate, setSelectedTemplate] =
useState<IReleasePlanTemplate>(); useState<IReleasePlanTemplate>();
const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false); const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false);
const [releasePlanPreview, setReleasePlanPreview] = useState(false);
const dialogId = isStrategyMenuDialogOpen const dialogId = isStrategyMenuDialogOpen
? 'FeatureStrategyMenuDialog' ? 'FeatureStrategyMenuDialog'
: undefined; : undefined;
@ -90,6 +97,11 @@ export const FeatureStrategyMenu = ({
setIsStrategyMenuDialogOpen(false); setIsStrategyMenuDialogOpen(false);
}; };
useEffect(() => {
if (!isStrategyMenuDialogOpen) return;
setReleasePlanPreview(false);
}, [isStrategyMenuDialogOpen]);
const openDefaultStrategyCreationModal = (event: React.SyntheticEvent) => { const openDefaultStrategyCreationModal = (event: React.SyntheticEvent) => {
trackEvent('strategy-add', { trackEvent('strategy-add', {
props: { props: {
@ -249,10 +261,36 @@ export const FeatureStrategyMenu = ({
sx: { sx: {
borderRadius: '12px', borderRadius: '12px',
height: newStrategyModalEnabled ? '100%' : 'auto', height: newStrategyModalEnabled ? '100%' : 'auto',
width: '100%',
}, },
}} }}
> >
{newStrategyModalEnabled ? ( {newStrategyModalEnabled ? (
<>
<StyledHeader>
<Typography variant='h2'>Add strategy</Typography>
<IconButton
size='small'
onClick={onClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
{releasePlanPreview && selectedTemplate ? (
<ReleasePlanPreview
template={selectedTemplate}
projectId={projectId}
featureName={featureId}
environment={environmentId}
crProtected={crProtected}
onBack={() => setReleasePlanPreview(false)}
onConfirm={() => {
addReleasePlan(selectedTemplate);
}}
/>
) : (
<FeatureStrategyMenuCards <FeatureStrategyMenuCards
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
@ -265,11 +303,12 @@ export const FeatureStrategyMenu = ({
}} }}
onReviewReleasePlan={(template) => { onReviewReleasePlan={(template) => {
setSelectedTemplate(template); setSelectedTemplate(template);
setAddReleasePlanOpen(true); setReleasePlanPreview(true);
onClose();
}} }}
onClose={onClose} onClose={onClose}
/> />
)}
</>
) : ( ) : (
<LegacyFeatureStrategyMenuCards <LegacyFeatureStrategyMenuCards
projectId={projectId} projectId={projectId}
@ -290,26 +329,6 @@ export const FeatureStrategyMenu = ({
)} )}
</Dialog> </Dialog>
{selectedTemplate && ( {selectedTemplate && (
<>
{newStrategyModalEnabled ? (
<ReleasePlanReviewDialog
open={addReleasePlanOpen}
setOpen={(open) => {
setAddReleasePlanOpen(open);
if (!open) {
setIsStrategyMenuDialogOpen(true);
}
}}
onConfirm={() => {
addReleasePlan(selectedTemplate);
}}
template={selectedTemplate}
projectId={projectId}
featureName={featureId}
environment={environmentId}
crProtected={crProtected}
/>
) : (
<LegacyReleasePlanReviewDialog <LegacyReleasePlanReviewDialog
open={addReleasePlanOpen} open={addReleasePlanOpen}
setOpen={(open) => { setOpen={(open) => {
@ -328,8 +347,6 @@ export const FeatureStrategyMenu = ({
crProtected={crProtected} crProtected={crProtected}
/> />
)} )}
</>
)}
</StyledStrategyMenu> </StyledStrategyMenu>
); );
}; };

View File

@ -1,16 +1,12 @@
import { styled, Typography, Box, IconButton } from '@mui/material'; import { styled, Box } from '@mui/material';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx'; import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { Link as RouterLink, useNavigate } from 'react-router-dom'; import { Link as RouterLink, useNavigate } from 'react-router-dom';
import CloseIcon from '@mui/icons-material/Close';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
import { type Dispatch, type SetStateAction, useContext, useMemo } from 'react'; import { type Dispatch, type SetStateAction, useContext, useMemo } from 'react';
import { import { FeatureStrategyMenuCardsSection } from './FeatureStrategyMenuCardsSection.tsx';
FeatureStrategyMenuCardsSection,
StyledStrategyModalSectionHeader,
} from './FeatureStrategyMenuCardsSection.tsx';
import { FeatureStrategyMenuCardsReleaseTemplates } from './FeatureStrategyMenuCardsReleaseTemplates.tsx'; import { FeatureStrategyMenuCardsReleaseTemplates } from './FeatureStrategyMenuCardsReleaseTemplates.tsx';
import { QuickFilters } from 'component/common/QuickFilters/QuickFilters.tsx'; import { QuickFilters } from 'component/common/QuickFilters/QuickFilters.tsx';
import { import {
@ -41,13 +37,12 @@ export type StrategyFilterValue = (typeof FILTERS)[number]['value'];
const CUSTOM_STRATEGY_DISPLAY_LIMIT = 5; const CUSTOM_STRATEGY_DISPLAY_LIMIT = 5;
const StyledContainer = styled(Box)(() => ({ const StyledContainer = styled(Box)(() => ({
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
})); }));
const StyledScrollableContent = styled(Box)(({ theme }) => ({ const StyledScrollableContent = styled(Box)(({ theme }) => ({
width: theme.breakpoints.values.md, width: '100%',
height: '100%', height: '100%',
overflowY: 'auto', overflowY: 'auto',
padding: theme.spacing(4), padding: theme.spacing(4),
@ -57,13 +52,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({
gap: theme.spacing(5), gap: theme.spacing(5),
})); }));
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4),
}));
const StyledFiltersContainer = styled(Box)(({ theme }) => ({ const StyledFiltersContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
padding: theme.spacing(0, 4, 3, 4), padding: theme.spacing(0, 4, 3, 4),
@ -214,17 +202,6 @@ export const FeatureStrategyMenuCards = ({
return ( return (
<StyledContainer> <StyledContainer>
<StyledHeader>
<Typography variant='h2'>Add strategy</Typography>
<IconButton
size='small'
onClick={onClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
<StyledFiltersContainer> <StyledFiltersContainer>
<QuickFilters <QuickFilters
filters={availableFilters} filters={availableFilters}
@ -233,31 +210,19 @@ export const FeatureStrategyMenuCards = ({
/> />
</StyledFiltersContainer> </StyledFiltersContainer>
<StyledScrollableContent> <StyledScrollableContent>
{(shouldRender('default') || shouldRender('standard')) && (
<Box>
<FeatureStrategyMenuCardsSection>
{shouldRender('default') && ( {shouldRender('default') && (
<StyledStrategyModalSectionHeader> <FeatureStrategyMenuCardsSection
<Typography color='inherit' variant='body2'> title={
<>
Project default Project default
</Typography>
<HelpIcon <HelpIcon
htmlTooltip htmlTooltip
tooltip={projectDefaultTooltip} tooltip={projectDefaultTooltip}
size='16px' size='16px'
/> />
</StyledStrategyModalSectionHeader> </>
)} }
{shouldRender('standard') && ( >
<StyledStrategyModalSectionHeader>
<Typography color='inherit' variant='body2'>
Standard strategies
</Typography>
</StyledStrategyModalSectionHeader>
)}
</FeatureStrategyMenuCardsSection>
<FeatureStrategyMenuCardsSection>
{shouldRender('default') && (
<FeatureStrategyMenuCardsDefaultStrategy <FeatureStrategyMenuCardsDefaultStrategy
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
@ -265,12 +230,12 @@ export const FeatureStrategyMenuCards = ({
onConfigure={onConfigure} onConfigure={onConfigure}
onClose={onClose} onClose={onClose}
/> />
</FeatureStrategyMenuCardsSection>
)} )}
{shouldRender('standard') && ( {shouldRender('standard') && (
<>{standardStrategies.map(renderStrategy)}</> <FeatureStrategyMenuCardsSection title='Standard strategies'>
)} {standardStrategies.map(renderStrategy)}
</FeatureStrategyMenuCardsSection> </FeatureStrategyMenuCardsSection>
</Box>
)} )}
{shouldRender('releaseTemplates') && ( {shouldRender('releaseTemplates') && (
<FeatureStrategyMenuCardsReleaseTemplates <FeatureStrategyMenuCardsReleaseTemplates

View File

@ -13,6 +13,12 @@ export const StyledStrategyModalSectionHeader = styled(Box)(({ theme }) => ({
const StyledStrategyModalSectionGrid = styled(Box)(({ theme }) => ({ const StyledStrategyModalSectionGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateColumns: 'repeat(3, 1fr)',
[theme.breakpoints.down('md')]: {
gridTemplateColumns: 'repeat(2, 1fr)',
},
[theme.breakpoints.down('sm')]: {
gridTemplateColumns: 'repeat(1, 1fr)',
},
gap: theme.spacing(2), gap: theme.spacing(2),
width: '100%', width: '100%',
})); }));
@ -28,21 +34,22 @@ const StyledViewMoreButton = styled(Button)(({ theme }) => ({
})); }));
interface IFeatureStrategyMenuCardsSectionProps { interface IFeatureStrategyMenuCardsSectionProps {
title?: string; title?: ReactNode;
limit?: number; limit?: number;
viewMore?: () => void; viewMore?: () => void;
viewMoreLabel?: string; viewMoreLabel?: string;
children: ReactNode[]; children: ReactNode;
} }
export const FeatureStrategyMenuCardsSection = ({ export const FeatureStrategyMenuCardsSection = ({
title, title,
limit, limit,
viewMore, viewMore,
viewMoreLabel = 'View more', viewMoreLabel = 'View more strategies',
children, children,
}: IFeatureStrategyMenuCardsSectionProps) => { }: IFeatureStrategyMenuCardsSectionProps) => {
const limitedChildren = limit ? children.slice(0, limit) : children; const allChildren = Array.isArray(children) ? children : [children];
const limitedChildren = limit ? allChildren.slice(0, limit) : allChildren;
return ( return (
<Box> <Box>
@ -53,7 +60,7 @@ export const FeatureStrategyMenuCardsSection = ({
)} )}
<StyledStrategyModalSectionGrid> <StyledStrategyModalSectionGrid>
{limitedChildren} {limitedChildren}
{viewMore && limitedChildren.length < children.length && ( {viewMore && limitedChildren.length < allChildren.length && (
<StyledViewMoreButton <StyledViewMoreButton
variant='text' variant='text'
size='small' size='small'

View File

@ -109,7 +109,6 @@ export const FeatureOverviewEnvironment = ({
featureId={featureId} featureId={featureId}
environmentId={environment.name} environmentId={environment.name}
variant='outlined' variant='outlined'
size='small'
/> />
) : ( ) : (
<FeatureOverviewEnvironmentMetrics <FeatureOverviewEnvironmentMetrics

View File

@ -6,22 +6,12 @@ import {
Typography, Typography,
Alert, Alert,
Box, Box,
IconButton,
Dialog,
DialogActions, DialogActions,
Button, Button,
} from '@mui/material'; } from '@mui/material';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import CloseIcon from '@mui/icons-material/Close';
const StyledDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': {
borderRadius: theme.shape.borderRadiusLarge,
height: '100%',
},
}));
const StyledScrollableContent = styled(Box)(({ theme }) => ({ const StyledScrollableContent = styled(Box)(({ theme }) => ({
width: theme.breakpoints.values.md, width: theme.breakpoints.values.md,
@ -31,13 +21,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({
flexDirection: 'column', flexDirection: 'column',
})); }));
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4),
}));
const StyledSubHeader = styled(Box)(({ theme }) => ({ const StyledSubHeader = styled(Box)(({ theme }) => ({
padding: theme.spacing(0, 3, 3, 3), padding: theme.spacing(0, 3, 3, 3),
})); }));
@ -50,29 +33,27 @@ const StyledDialogActions = styled(DialogActions)(({ theme }) => ({
padding: theme.spacing(2, 4, 4), padding: theme.spacing(2, 4, 4),
})); }));
interface IReleasePlanAddDialogProps { interface IReleasePlanPreviewProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: () => void;
template: IReleasePlanTemplate; template: IReleasePlanTemplate;
projectId: string; projectId: string;
featureName: string; featureName: string;
environment: string; environment: string;
crProtected?: boolean; crProtected?: boolean;
onConfirm: () => void;
onBack: () => void;
} }
export const ReleasePlanReviewDialog = ({ export const ReleasePlanPreview = ({
open,
setOpen,
onConfirm,
template, template,
projectId, projectId,
featureName, featureName,
environment, environment,
crProtected, crProtected,
}: IReleasePlanAddDialogProps) => { onConfirm,
onBack,
}: IReleasePlanPreviewProps) => {
const { feature } = useFeature(projectId, featureName); const { feature } = useFeature(projectId, featureName);
const { releasePlans } = useReleasePlans( const { releasePlans, loading } = useReleasePlans(
projectId, projectId,
featureName, featureName,
environment, environment,
@ -91,23 +72,12 @@ export const ReleasePlanReviewDialog = ({
environment, environment,
); );
const handleClose = () => setOpen(false); if (loading) return null;
return ( return (
<StyledDialog open={open} onClose={handleClose} fullWidth maxWidth='md'> <>
<StyledHeader>
<Typography variant='h2'>Add strategy</Typography>
<IconButton
size='small'
onClick={handleClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
<StyledSubHeader> <StyledSubHeader>
<Button variant='text' onClick={handleClose}> <Button variant='text' onClick={onBack}>
<StyledBackIcon /> <StyledBackIcon />
Go back Go back
</Button> </Button>
@ -146,6 +116,6 @@ export const ReleasePlanReviewDialog = ({
{crProtected ? 'Add suggestion to draft' : 'Apply template'} {crProtected ? 'Add suggestion to draft' : 'Apply template'}
</Button> </Button>
</StyledDialogActions> </StyledDialogActions>
</StyledDialog> </>
); );
}; };