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:
parent
99c4f7111a
commit
b865ee44f3
@ -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,10 +261,36 @@ export const FeatureStrategyMenu = ({
|
||||
sx: {
|
||||
borderRadius: '12px',
|
||||
height: newStrategyModalEnabled ? '100%' : 'auto',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{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
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
@ -265,11 +303,12 @@ export const FeatureStrategyMenu = ({
|
||||
}}
|
||||
onReviewReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
setAddReleasePlanOpen(true);
|
||||
onClose();
|
||||
setReleasePlanPreview(true);
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<LegacyFeatureStrategyMenuCards
|
||||
projectId={projectId}
|
||||
@ -290,26 +329,6 @@ 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) => {
|
||||
@ -328,8 +347,6 @@ export const FeatureStrategyMenu = ({
|
||||
crProtected={crProtected}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledStrategyMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,31 +210,19 @@ export const FeatureStrategyMenuCards = ({
|
||||
/>
|
||||
</StyledFiltersContainer>
|
||||
<StyledScrollableContent>
|
||||
{(shouldRender('default') || shouldRender('standard')) && (
|
||||
<Box>
|
||||
<FeatureStrategyMenuCardsSection>
|
||||
{shouldRender('default') && (
|
||||
<StyledStrategyModalSectionHeader>
|
||||
<Typography color='inherit' variant='body2'>
|
||||
<FeatureStrategyMenuCardsSection
|
||||
title={
|
||||
<>
|
||||
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}
|
||||
@ -265,12 +230,12 @@ export const FeatureStrategyMenuCards = ({
|
||||
onConfigure={onConfigure}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</FeatureStrategyMenuCardsSection>
|
||||
)}
|
||||
{shouldRender('standard') && (
|
||||
<>{standardStrategies.map(renderStrategy)}</>
|
||||
)}
|
||||
<FeatureStrategyMenuCardsSection title='Standard strategies'>
|
||||
{standardStrategies.map(renderStrategy)}
|
||||
</FeatureStrategyMenuCardsSection>
|
||||
</Box>
|
||||
)}
|
||||
{shouldRender('releaseTemplates') && (
|
||||
<FeatureStrategyMenuCardsReleaseTemplates
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -109,7 +109,6 @@ export const FeatureOverviewEnvironment = ({
|
||||
featureId={featureId}
|
||||
environmentId={environment.name}
|
||||
variant='outlined'
|
||||
size='small'
|
||||
/>
|
||||
) : (
|
||||
<FeatureOverviewEnvironmentMetrics
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user