1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00
unleash.unleash/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx
Nuno Góis b865ee44f3
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
2025-09-26 14:54:08 +01:00

353 lines
14 KiB
TypeScript

import type React 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 { 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';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import useToast from 'hooks/useToast';
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
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 { ReleasePlanPreview } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx';
import {
FeatureStrategyMenuCards,
type StrategyFilterValue,
} from './FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
interface IFeatureStrategyMenuProps {
label: string;
projectId: string;
featureId: string;
environmentId: string;
variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean;
disableReason?: string;
}
const StyledStrategyMenu = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row',
justifyContent: 'flex-end',
gap: theme.spacing(1),
}));
const StyledAdditionalMenuButton = styled(PermissionButton)(({ theme }) => ({
minWidth: 0,
width: theme.spacing(4.5),
alignSelf: 'stretch',
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,
matchWidth,
disableReason,
}: IFeatureStrategyMenuProps) => {
const [isStrategyMenuDialogOpen, setIsStrategyMenuDialogOpen] =
useState<boolean>(false);
const [onlyReleasePlans, setOnlyReleasePlans] = useState<boolean>(false);
const [filter, setFilter] = useState<StrategyFilterValue>(null);
const navigate = useNavigate();
const { trackEvent } = usePlausibleTracker();
const [selectedTemplate, setSelectedTemplate] =
useState<IReleasePlanTemplate>();
const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false);
const [releasePlanPreview, setReleasePlanPreview] = useState(false);
const dialogId = isStrategyMenuDialogOpen
? 'FeatureStrategyMenuDialog'
: undefined;
const { setToastApiError, setToastData } = useToast();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const { refetch } = useReleasePlans(projectId, featureId, environmentId);
const { addReleasePlanToFeature } = useReleasePlansApi();
const { isEnterprise } = useUiConfig();
const displayReleasePlanButton = isEnterprise();
const crProtected = isChangeRequestConfigured(environmentId);
const newStrategyModalEnabled = useUiFlag('newStrategyModal');
const onClose = () => {
setIsStrategyMenuDialogOpen(false);
};
useEffect(() => {
if (!isStrategyMenuDialogOpen) return;
setReleasePlanPreview(false);
}, [isStrategyMenuDialogOpen]);
const openDefaultStrategyCreationModal = (event: React.SyntheticEvent) => {
trackEvent('strategy-add', {
props: {
buttonTitle: label,
},
});
navigate(createStrategyPath);
};
const openMoreStrategies = (event: React.SyntheticEvent) => {
setOnlyReleasePlans(false);
setIsStrategyMenuDialogOpen(true);
};
const openReleasePlans = (event: React.SyntheticEvent) => {
setOnlyReleasePlans(true);
setIsStrategyMenuDialogOpen(true);
};
const addReleasePlan = async (template: IReleasePlanTemplate) => {
try {
if (crProtected) {
await addChange(projectId, environmentId, {
feature: featureId,
action: 'addReleasePlan',
payload: {
templateId: template.id,
},
});
setToastData({
type: 'success',
text: 'Added to draft',
});
refetchChangeRequests();
} else {
await addReleasePlanToFeature(
featureId,
template.id,
projectId,
environmentId,
);
setToastData({
type: 'success',
text: 'Release plan added',
});
refetch();
}
trackEvent('release-management', {
props: {
eventType: 'add-plan',
plan: template.name,
},
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
} finally {
setAddReleasePlanOpen(false);
setSelectedTemplate(undefined);
onClose();
}
};
const createStrategyPath = formatCreateStrategyPath(
projectId,
featureId,
environmentId,
'flexibleRollout',
true,
);
return (
<StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
{newStrategyModalEnabled ? (
<PermissionButton
data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openMoreStrategies}
aria-labelledby={dialogId}
variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
}}
>
Add strategy
</PermissionButton>
) : (
<>
{displayReleasePlanButton ? (
<PermissionButton
data-testid='ADD_TEMPLATE_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openReleasePlans}
aria-labelledby={dialogId}
variant='outlined'
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason
? disableReason
: undefined,
}}
>
Use template
</PermissionButton>
) : null}
<PermissionButton
data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openDefaultStrategyCreationModal}
aria-labelledby={dialogId}
variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
}}
>
{label}
</PermissionButton>
<StyledAdditionalMenuButton
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openMoreStrategies}
variant='outlined'
hideLockIcon
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason
? disableReason
: 'More strategies',
}}
>
<MoreVert />
</StyledAdditionalMenuButton>
</>
)}
<Dialog
open={isStrategyMenuDialogOpen}
onClose={onClose}
maxWidth='md'
PaperProps={{
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}
environmentId={environmentId}
filter={filter}
setFilter={setFilter}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setReleasePlanPreview(true);
}}
onClose={onClose}
/>
)}
</>
) : (
<LegacyFeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onlyReleasePlans={onlyReleasePlans}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setAddReleasePlanOpen(true);
onClose();
}}
onClose={onClose}
/>
)}
</Dialog>
{selectedTemplate && (
<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>
);
};