1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

fix: strategy remove menu (#3807)

![image](https://github.com/Unleash/unleash/assets/2625371/6d0f0c58-3637-4586-a0c4-aeb45e5e4a2c)

Menu was hindering the rendering of confirmation dialog.
https://linear.app/unleash/issue/1-935/fix-feature-strategy-actions-menu
This commit is contained in:
Tymoteusz Czech 2023-05-18 16:35:46 +02:00 committed by GitHub
parent 116c94ae49
commit 149c54b0b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 438 additions and 320 deletions

View File

@ -1,179 +0,0 @@
import { VFC, useState } from 'react';
import { Alert, Typography } from '@mui/material';
import BlockIcon from '@mui/icons-material/Block';
import TrackChangesIcon from '@mui/icons-material/TrackChanges';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE_STRATEGY } from '@server/types/permissions';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { useEnableDisable } from './hooks/useEnableDisable';
import { useSuggestEnableDisable } from './hooks/useSuggestEnableDisable';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyChangeRequestAlert } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert';
import { IDisableEnableStrategyProps } from './IDisableEnableStrategyProps';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
const DisableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => {
const { projectId, environmentId, featureId } = props;
const [isDialogueOpen, setDialogueOpen] = useState(false);
const { onDisable } = useEnableDisable({ ...props });
const { onSuggestDisable } = useSuggestEnableDisable({ ...props });
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const isChangeRequest = isChangeRequestConfigured(environmentId);
const { refetchFeature } = useFeature(projectId, featureId);
const onClick = (event: React.FormEvent) => {
event.preventDefault();
if (isChangeRequest) {
onSuggestDisable();
} else {
onDisable();
refetchFeature();
}
setDialogueOpen(false);
};
return (
<>
<PermissionIconButton
onClick={() => setDialogueOpen(true)}
projectId={projectId}
environmentId={environmentId}
permission={UPDATE_FEATURE_STRATEGY}
tooltipProps={{
title: 'Disable strategy',
}}
type="button"
>
<BlockIcon />
<ConditionallyRender
condition={Boolean(props.text)}
show={
<Typography
variant={'body1'}
color={'text.secondary'}
sx={{ ml: theme => theme.spacing(1) }}
>
Disable
</Typography>
}
/>
</PermissionIconButton>
<Dialogue
title={
isChangeRequest
? 'Add disabling strategy to change request?'
: 'Are you sure you want to disable this strategy?'
}
open={isDialogueOpen}
primaryButtonText={
isChangeRequest ? 'Add to draft' : 'Disable strategy'
}
secondaryButtonText="Cancel"
onClick={onClick}
onClose={() => setDialogueOpen(false)}
>
<ConditionallyRender
condition={isChangeRequest}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
}
elseShow={
<Alert severity="error">
Disabling the strategy will change which users
receive access to the feature.
</Alert>
}
/>
</Dialogue>
</>
);
};
const EnableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => {
const { projectId, environmentId } = props;
const [isDialogueOpen, setDialogueOpen] = useState(false);
const { onEnable } = useEnableDisable({ ...props });
const { onSuggestEnable } = useSuggestEnableDisable({ ...props });
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const isChangeRequest = isChangeRequestConfigured(environmentId);
const onClick = (event: React.FormEvent) => {
event.preventDefault();
if (isChangeRequest) {
onSuggestEnable();
} else {
onEnable();
}
setDialogueOpen(false);
};
return (
<>
<PermissionIconButton
onClick={() => setDialogueOpen(true)}
projectId={projectId}
environmentId={environmentId}
permission={UPDATE_FEATURE_STRATEGY}
tooltipProps={{
title: 'Enable strategy',
}}
type="button"
>
<TrackChangesIcon />
<ConditionallyRender
condition={Boolean(props.text)}
show={
<Typography
variant={'body1'}
color={'text.secondary'}
sx={{ ml: theme => theme.spacing(1) }}
>
Disable
</Typography>
}
/>
</PermissionIconButton>
<Dialogue
title={
isChangeRequest
? 'Add enabling strategy to change request?'
: 'Are you sure you want to enable this strategy?'
}
open={isDialogueOpen}
primaryButtonText={
isChangeRequest ? 'Add to draft' : 'Enable strategy'
}
secondaryButtonText="Cancel"
onClick={onClick}
onClose={() => setDialogueOpen(false)}
>
<ConditionallyRender
condition={isChangeRequest}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
}
elseShow={
<Alert severity="error">
Enabling the strategy will change which users
receive access to the feature.
</Alert>
}
/>
</Dialogue>
</>
);
};
export const DisableEnableStrategy: VFC<IDisableEnableStrategyProps> = ({
...props
}) =>
props.strategy.disabled ? (
<EnableStrategy {...props} />
) : (
<DisableStrategy {...props} />
);

View File

@ -3,7 +3,7 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe
import { formatUnknownError } from 'utils/formatUnknownError';
import { useNavigate } from 'react-router-dom';
import useToast from 'hooks/useToast';
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
import { formatFeaturePath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { Alert, styled, Typography } from '@mui/material';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
@ -157,7 +157,11 @@ const useOnSuggestRemove = ({
return onSuggestRemove;
};
export const FeatureStrategyRemove = ({
/**
* @deprecated
* TODO: remove when strategyImprovements flag is removed
*/
export const LegacyFeatureStrategyRemove = ({
projectId,
featureId,
environmentId,

View File

@ -0,0 +1,201 @@
import React, { FC } from 'react';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useNavigate } from 'react-router-dom';
import useToast from 'hooks/useToast';
import { formatFeaturePath } from '../../../../../../../../FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { Alert, styled, Typography } from '@mui/material';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
interface IFeatureStrategyRemoveProps {
projectId: string;
featureId: string;
environmentId: string;
strategyId: string;
disabled?: boolean;
icon?: boolean;
text?: boolean;
}
interface IFeatureStrategyRemoveDialogueProps {
onRemove: (event: React.FormEvent) => Promise<void>;
onClose: () => void;
isOpen: boolean;
}
const RemoveAlert: FC = () => (
<Alert severity="error">
Removing the strategy will change which users receive access to the
feature.
</Alert>
);
const FeatureStrategyRemoveDialogue: FC<
IFeatureStrategyRemoveDialogueProps
> = ({ onRemove, onClose, isOpen }) => {
return (
<Dialogue
title="Are you sure you want to delete this strategy?"
open={isOpen}
primaryButtonText="Remove strategy"
secondaryButtonText="Cancel"
onClick={onRemove}
onClose={onClose}
>
<RemoveAlert />
</Dialogue>
);
};
const MsgContainer = styled('div')(({ theme }) => ({
marginTop: theme.spacing(3),
marginBottom: theme.spacing(1),
}));
const SuggestFeatureStrategyRemoveDialogue: FC<
IFeatureStrategyRemoveDialogueProps
> = ({ onRemove, onClose, isOpen }) => {
return (
<Dialogue
title="Suggest changes"
open={isOpen}
primaryButtonText="Add suggestion to draft"
secondaryButtonText="Cancel"
onClick={onRemove}
onClose={onClose}
>
<RemoveAlert />
<MsgContainer>
<Typography variant="body2" color="text.secondary">
Your suggestion:
</Typography>
</MsgContainer>
<Typography fontWeight="bold">Remove strategy</Typography>
</Dialogue>
);
};
interface IRemoveProps {
projectId: string;
featureId: string;
environmentId: string;
strategyId: string;
}
const useOnRemove = ({
projectId,
featureId,
environmentId,
strategyId,
}: IRemoveProps) => {
const { deleteStrategyFromFeature } = useFeatureStrategyApi();
const { setToastData, setToastApiError } = useToast();
const navigate = useNavigate();
const { refetchFeature } = useFeature(projectId, featureId);
const onRemove = async (event: React.FormEvent) => {
try {
event.preventDefault();
await deleteStrategyFromFeature(
projectId,
featureId,
environmentId,
strategyId
);
setToastData({
title: 'Strategy deleted',
type: 'success',
});
refetchFeature();
navigate(formatFeaturePath(projectId, featureId));
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return onRemove;
};
const useOnSuggestRemove = ({
projectId,
featureId,
environmentId,
strategyId,
}: IRemoveProps) => {
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const { setToastData, setToastApiError } = useToast();
const onSuggestRemove = async (event: React.FormEvent) => {
try {
event.preventDefault();
await addChange(projectId, environmentId, {
action: 'deleteStrategy',
feature: featureId,
payload: {
id: strategyId,
},
});
setToastData({
title: 'Changes added to the draft!',
type: 'success',
});
await refetchChangeRequests();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return onSuggestRemove;
};
export const DialogStrategyRemove = ({
projectId,
featureId,
environmentId,
strategyId,
text,
isOpen,
onClose,
}: IFeatureStrategyRemoveProps & {
isOpen: boolean;
onClose: () => void;
}) => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const onRemove = useOnRemove({
featureId,
projectId,
strategyId,
environmentId,
});
const onSuggestRemove = useOnSuggestRemove({
featureId,
projectId,
strategyId,
environmentId,
});
if (isChangeRequestConfigured(environmentId)) {
return (
<SuggestFeatureStrategyRemoveDialogue
isOpen={isOpen}
onClose={() => onClose()}
onRemove={async e => {
await onSuggestRemove(e);
onClose();
}}
/>
);
}
return (
<FeatureStrategyRemoveDialogue
isOpen={isOpen}
onClose={() => onClose()}
onRemove={onRemove}
/>
);
};

View File

@ -0,0 +1,78 @@
import { Alert } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { useEnableDisable } from './hooks/useEnableDisable';
import { useSuggestEnableDisable } from './hooks/useSuggestEnableDisable';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyChangeRequestAlert } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert';
import { IDisableEnableStrategyProps } from './IDisableEnableStrategyProps';
export const DisableEnableStrategyDialog = ({
isOpen,
onClose,
...props
}: IDisableEnableStrategyProps & {
isOpen: boolean;
onClose: () => void;
}) => {
const { projectId, environmentId } = props;
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const isChangeRequest = isChangeRequestConfigured(environmentId);
const { onSuggestEnable } = useSuggestEnableDisable({ ...props });
const { onEnable, onDisable } = useEnableDisable({ ...props });
const disabled = Boolean(props.strategy?.disabled);
const onClick = (event: React.FormEvent) => {
event.preventDefault();
if (isChangeRequest) {
if (disabled) {
onSuggestEnable();
} else {
onSuggestEnable();
}
} else {
if (disabled) {
onEnable();
} else {
onDisable();
}
}
onClose();
};
return (
<Dialogue
title={
isChangeRequest
? `Add ${
disabled ? 'enable' : 'disable'
} strategy to change request?`
: 'Are you sure you want to enable this strategy?'
}
open={isOpen}
primaryButtonText={
isChangeRequest
? 'Add to draft'
: `${disabled ? 'Enable' : 'Disable'} strategy`
}
secondaryButtonText="Cancel"
onClick={onClick}
onClose={() => onClose()}
>
<ConditionallyRender
condition={isChangeRequest}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
}
elseShow={
<Alert severity="error">
Enabling the strategy will change which users receive
access to the feature.
</Alert>
}
/>
</Dialogue>
);
};

View File

@ -0,0 +1,149 @@
import React, { SyntheticEvent, useState } from 'react';
import {
Box,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { IFeatureStrategy } from 'interfaces/strategy';
import { DialogStrategyRemove } from './DialogStrategyRemove';
import { DisableEnableStrategyDialog } from './DisableEnableStrategyDialog/DisableEnableStrategyDialog';
import TrackChangesIcon from '@mui/icons-material/TrackChanges';
import DeleteIcon from '@mui/icons-material/Delete';
import BlockIcon from '@mui/icons-material/Block';
import {
DELETE_FEATURE_STRATEGY,
UPDATE_FEATURE_STRATEGY,
} from '@server/types/permissions';
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
import { STRATEGY_FORM_REMOVE_ID } from 'utils/testIds';
export interface IRemoveStrategyMenuProps {
projectId: string;
featureId: string;
environmentId: string;
strategy: IFeatureStrategy;
}
const MenuStrategyRemove = ({
projectId,
strategy,
featureId,
environmentId,
}: IRemoveStrategyMenuProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [isDisableEnableDialogOpen, setDisableEnableDialogOpen] =
useState(false);
const [isRemoveDialogOpen, setRemoveDialogOpen] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (event: SyntheticEvent) => {
setAnchorEl(null);
event.stopPropagation();
};
const updateAccess = useHasProjectEnvironmentAccess(
UPDATE_FEATURE_STRATEGY,
projectId,
environmentId
);
const deleteAccess = useHasProjectEnvironmentAccess(
DELETE_FEATURE_STRATEGY,
projectId,
environmentId
);
return (
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
textAlign: 'center',
}}
>
<Tooltip title="More actions">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'actions-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<MoreVertIcon sx={{ width: 32, height: 32 }} />
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="actions-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<Tooltip
title={
strategy.disabled
? 'Enable strategy'
: 'Disable strategy'
}
arrow
placement="left"
>
<MenuItem
disabled={!updateAccess}
onClick={() => setDisableEnableDialogOpen(true)}
>
<ListItemIcon>
{strategy.disabled ? (
<TrackChangesIcon />
) : (
<BlockIcon />
)}
</ListItemIcon>
<ListItemText>
{strategy.disabled ? 'Enable' : 'Disable'}
</ListItemText>
</MenuItem>
</Tooltip>
<Tooltip title="Remove strategy" arrow placement="left">
<MenuItem
disabled={!deleteAccess}
onClick={() => setRemoveDialogOpen(true)}
data-testid={STRATEGY_FORM_REMOVE_ID}
>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText>Remove</ListItemText>
</MenuItem>
</Tooltip>
</Menu>
<DisableEnableStrategyDialog
isOpen={isDisableEnableDialogOpen}
onClose={() => setDisableEnableDialogOpen(false)}
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
/>
<DialogStrategyRemove
isOpen={isRemoveDialogOpen}
onClose={() => setRemoveDialogOpen(false)}
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategyId={strategy.id}
/>
</>
);
};
export default MenuStrategyRemove;

View File

@ -1,134 +0,0 @@
import React, { SyntheticEvent } from 'react';
import {
Avatar,
Box,
IconButton,
ListItem,
Menu,
MenuItem,
styled,
Tooltip,
Typography,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { IFeatureStrategy } from '../../../../../../../../../../interfaces/strategy';
import { FeatureStrategyRemove } from '../../../../../../../../FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
import { DisableEnableStrategy } from '../DisableEnableStrategy/DisableEnableStrategy';
export interface IRemoveStrategyMenuProps {
projectId: string;
featureId: string;
environmentId: string;
strategy: IFeatureStrategy;
}
const StyledContainer = styled(ListItem)(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
minWidth: 'fit-content',
padding: theme.spacing(0, 2),
}));
const RemoveStrategyMenu = ({
projectId,
strategy,
featureId,
environmentId,
}: IRemoveStrategyMenuProps) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (event: SyntheticEvent) => {
setAnchorEl(null);
event.stopPropagation();
};
return (
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
textAlign: 'center',
}}
>
<Tooltip title="More actions">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'actions-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<MoreVertIcon sx={{ width: 32, height: 32 }} />
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="actions-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
pl: 0.5,
minWidth: 'fit-content',
justifyContent: 'center',
li: {
pl: 0,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem
component={() => (
<StyledContainer>
<DisableEnableStrategy
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
text
/>
</StyledContainer>
)}
/>
<MenuItem
component={() => (
<FeatureStrategyRemove
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategyId={strategy.id}
text
icon
/>
)}
/>
</Menu>
</>
);
};
export default RemoveStrategyMenu;

View File

@ -6,15 +6,14 @@ import { IFeatureStrategy } from 'interfaces/strategy';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
import { DisableEnableStrategy } from './DisableEnableStrategy/DisableEnableStrategy';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import RemoveStrategyMenu from './RemoveStrategyMenu/RemoveStrategyMenu';
import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove';
import { LegacyFeatureStrategyRemove } from './LegacyFeatureStrategyRemove';
interface IStrategyItemProps {
environmentId: string;
@ -85,7 +84,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
uiConfig?.flags?.strategyImprovements
)}
show={() => (
<RemoveStrategyMenu
<MenuStrategyRemove
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
@ -93,7 +92,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
/>
)}
elseShow={() => (
<FeatureStrategyRemove
<LegacyFeatureStrategyRemove
projectId={projectId}
featureId={featureId}
environmentId={environmentId}