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 { useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PermissionButton, {
type IPermissionButtonProps,
} from 'component/common/PermissionButton/PermissionButton';
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 { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate.tsx';
import MoreVert from '@mui/icons-material/MoreVert';
import CloseIcon from '@mui/icons-material/Close';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
@ -20,7 +21,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { formatUnknownError } from 'utils/formatUnknownError';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
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 {
FeatureStrategyMenuCards,
type StrategyFilterValue,
@ -34,7 +35,6 @@ interface IFeatureStrategyMenuProps {
environmentId: string;
variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean;
size?: IPermissionButtonProps['size'];
disableReason?: string;
}
@ -52,13 +52,19 @@ const StyledAdditionalMenuButton = styled(PermissionButton)(({ theme }) => ({
paddingBlock: 0,
}));
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4),
}));
export const FeatureStrategyMenu = ({
label,
projectId,
featureId,
environmentId,
variant,
size,
matchWidth,
disableReason,
}: IFeatureStrategyMenuProps) => {
@ -71,6 +77,7 @@ export const FeatureStrategyMenu = ({
const [selectedTemplate, setSelectedTemplate] =
useState<IReleasePlanTemplate>();
const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false);
const [releasePlanPreview, setReleasePlanPreview] = useState(false);
const dialogId = isStrategyMenuDialogOpen
? 'FeatureStrategyMenuDialog'
: undefined;
@ -90,6 +97,11 @@ export const FeatureStrategyMenu = ({
setIsStrategyMenuDialogOpen(false);
};
useEffect(() => {
if (!isStrategyMenuDialogOpen) return;
setReleasePlanPreview(false);
}, [isStrategyMenuDialogOpen]);
const openDefaultStrategyCreationModal = (event: React.SyntheticEvent) => {
trackEvent('strategy-add', {
props: {
@ -249,27 +261,54 @@ export const FeatureStrategyMenu = ({
sx: {
borderRadius: '12px',
height: newStrategyModalEnabled ? '100%' : 'auto',
width: '100%',
},
}}
>
{newStrategyModalEnabled ? (
<FeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
filter={filter}
setFilter={setFilter}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setAddReleasePlanOpen(true);
onClose();
}}
onClose={onClose}
/>
<>
<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
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
filter={filter}
setFilter={setFilter}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setReleasePlanPreview(true);
}}
onClose={onClose}
/>
)}
</>
) : (
<LegacyFeatureStrategyMenuCards
projectId={projectId}
@ -290,45 +329,23 @@ export const FeatureStrategyMenu = ({
)}
</Dialog>
{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
open={addReleasePlanOpen}
setOpen={(open) => {
setAddReleasePlanOpen(open);
if (!open) {
setIsStrategyMenuDialogOpen(true);
}
}}
onConfirm={() => {
addReleasePlan(selectedTemplate);
}}
template={selectedTemplate}
projectId={projectId}
featureName={featureId}
environment={environmentId}
crProtected={crProtected}
/>
)}
</>
<LegacyReleasePlanReviewDialog
open={addReleasePlanOpen}
setOpen={(open) => {
setAddReleasePlanOpen(open);
if (!open) {
setIsStrategyMenuDialogOpen(true);
}
}}
onConfirm={() => {
addReleasePlan(selectedTemplate);
}}
template={selectedTemplate}
projectId={projectId}
featureName={featureId}
environment={environmentId}
crProtected={crProtected}
/>
)}
</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 { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
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 { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
import { type Dispatch, type SetStateAction, useContext, useMemo } from 'react';
import {
FeatureStrategyMenuCardsSection,
StyledStrategyModalSectionHeader,
} from './FeatureStrategyMenuCardsSection.tsx';
import { FeatureStrategyMenuCardsSection } from './FeatureStrategyMenuCardsSection.tsx';
import { FeatureStrategyMenuCardsReleaseTemplates } from './FeatureStrategyMenuCardsReleaseTemplates.tsx';
import { QuickFilters } from 'component/common/QuickFilters/QuickFilters.tsx';
import {
@ -41,13 +37,12 @@ export type StrategyFilterValue = (typeof FILTERS)[number]['value'];
const CUSTOM_STRATEGY_DISPLAY_LIMIT = 5;
const StyledContainer = styled(Box)(() => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
}));
const StyledScrollableContent = styled(Box)(({ theme }) => ({
width: theme.breakpoints.values.md,
width: '100%',
height: '100%',
overflowY: 'auto',
padding: theme.spacing(4),
@ -57,13 +52,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({
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 }) => ({
display: 'flex',
padding: theme.spacing(0, 4, 3, 4),
@ -214,17 +202,6 @@ export const FeatureStrategyMenuCards = ({
return (
<StyledContainer>
<StyledHeader>
<Typography variant='h2'>Add strategy</Typography>
<IconButton
size='small'
onClick={onClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
<StyledFiltersContainer>
<QuickFilters
filters={availableFilters}
@ -233,44 +210,32 @@ export const FeatureStrategyMenuCards = ({
/>
</StyledFiltersContainer>
<StyledScrollableContent>
{(shouldRender('default') || shouldRender('standard')) && (
<Box>
<FeatureStrategyMenuCardsSection>
{shouldRender('default') && (
<StyledStrategyModalSectionHeader>
<Typography color='inherit' variant='body2'>
Project default
</Typography>
<HelpIcon
htmlTooltip
tooltip={projectDefaultTooltip}
size='16px'
/>
</StyledStrategyModalSectionHeader>
)}
{shouldRender('standard') && (
<StyledStrategyModalSectionHeader>
<Typography color='inherit' variant='body2'>
Standard strategies
</Typography>
</StyledStrategyModalSectionHeader>
)}
</FeatureStrategyMenuCardsSection>
<FeatureStrategyMenuCardsSection>
{shouldRender('default') && (
<FeatureStrategyMenuCardsDefaultStrategy
projectId={projectId}
environmentId={environmentId}
featureId={featureId}
onConfigure={onConfigure}
onClose={onClose}
{shouldRender('default') && (
<FeatureStrategyMenuCardsSection
title={
<>
Project default
<HelpIcon
htmlTooltip
tooltip={projectDefaultTooltip}
size='16px'
/>
)}
{shouldRender('standard') && (
<>{standardStrategies.map(renderStrategy)}</>
)}
</FeatureStrategyMenuCardsSection>
</Box>
</>
}
>
<FeatureStrategyMenuCardsDefaultStrategy
projectId={projectId}
environmentId={environmentId}
featureId={featureId}
onConfigure={onConfigure}
onClose={onClose}
/>
</FeatureStrategyMenuCardsSection>
)}
{shouldRender('standard') && (
<FeatureStrategyMenuCardsSection title='Standard strategies'>
{standardStrategies.map(renderStrategy)}
</FeatureStrategyMenuCardsSection>
)}
{shouldRender('releaseTemplates') && (
<FeatureStrategyMenuCardsReleaseTemplates

View File

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

View File

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

View File

@ -6,22 +6,12 @@ import {
Typography,
Alert,
Box,
IconButton,
Dialog,
DialogActions,
Button,
} from '@mui/material';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
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 }) => ({
width: theme.breakpoints.values.md,
@ -31,13 +21,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({
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 }) => ({
padding: theme.spacing(0, 3, 3, 3),
}));
@ -50,29 +33,27 @@ const StyledDialogActions = styled(DialogActions)(({ theme }) => ({
padding: theme.spacing(2, 4, 4),
}));
interface IReleasePlanAddDialogProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: () => void;
interface IReleasePlanPreviewProps {
template: IReleasePlanTemplate;
projectId: string;
featureName: string;
environment: string;
crProtected?: boolean;
onConfirm: () => void;
onBack: () => void;
}
export const ReleasePlanReviewDialog = ({
open,
setOpen,
onConfirm,
export const ReleasePlanPreview = ({
template,
projectId,
featureName,
environment,
crProtected,
}: IReleasePlanAddDialogProps) => {
onConfirm,
onBack,
}: IReleasePlanPreviewProps) => {
const { feature } = useFeature(projectId, featureName);
const { releasePlans } = useReleasePlans(
const { releasePlans, loading } = useReleasePlans(
projectId,
featureName,
environment,
@ -91,23 +72,12 @@ export const ReleasePlanReviewDialog = ({
environment,
);
const handleClose = () => setOpen(false);
if (loading) return null;
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>
<Button variant='text' onClick={handleClose}>
<Button variant='text' onClick={onBack}>
<StyledBackIcon />
Go back
</Button>
@ -146,6 +116,6 @@ export const ReleasePlanReviewDialog = ({
{crProtected ? 'Add suggestion to draft' : 'Apply template'}
</Button>
</StyledDialogActions>
</StyledDialog>
</>
);
};