1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

fix: generalize multi action button (#6294)

This PR moves the CR specific logic out of the MultiActionButton and
generalises so that we can re-use it across the application. The CR
specific logic is moved into:

* ApplyButton.tsx
* ReviewButton.tsx

This fixes a bug where multi action button would be disabled if you
tried to apply an approved change request that you had created yourself.
This commit is contained in:
Fredrik Strand Oseberg 2024-02-21 14:37:35 +01:00 committed by GitHub
parent ac183e76f8
commit 0ccfc29e26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 138 additions and 64 deletions

View File

@ -2,34 +2,45 @@ import { FC } from 'react';
import CheckBox from '@mui/icons-material/Check';
import Today from '@mui/icons-material/Today';
import { MultiActionButton } from '../MultiActionButton/MultiActionButton';
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
import { MultiActionButton } from 'component/common/MultiActionButton/MultiActionButton';
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
export const ApplyButton: FC<{
disabled: boolean;
onSchedule: () => void;
onApply: () => void;
variant?: 'create' | 'update';
}> = ({ disabled, onSchedule, onApply, variant = 'create', children }) => (
<MultiActionButton
permission={APPLY_CHANGE_REQUEST}
disabled={disabled}
actions={[
{
label: 'Apply changes',
onSelect: onApply,
icon: <CheckBox fontSize='small' />,
},
{
label:
variant === 'create'
? 'Schedule changes'
: 'Update schedule',
onSelect: onSchedule,
icon: <Today fontSize='small' />,
},
]}
>
{children}
</MultiActionButton>
);
}> = ({ disabled, onSchedule, onApply, variant = 'create', children }) => {
const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id');
const { data } = useChangeRequest(projectId, id);
return (
<MultiActionButton
permission={APPLY_CHANGE_REQUEST}
disabled={disabled}
actions={[
{
label: 'Apply changes',
onSelect: onApply,
icon: <CheckBox fontSize='small' />,
},
{
label:
variant === 'create'
? 'Schedule changes'
: 'Update schedule',
onSelect: onSchedule,
icon: <Today fontSize='small' />,
},
]}
environmentId={data?.environment}
projectId={projectId}
ariaLabel='apply or schedule changes'
>
{children}
</MultiActionButton>
);
};

View File

@ -126,11 +126,17 @@ const changeRequestConfig = () =>
'get',
);
const setupChangeRequest = (featureName: string, state: ChangeRequestState) => {
pendingChangeRequest(mockChangeRequest(featureName, state));
changeRequest(mockChangeRequest(featureName, state));
const setupChangeRequest = (
featureName: string,
state: ChangeRequestState,
overrides: Partial<Omit<ChangeRequestType, 'state' | 'schedule'>> = {},
) => {
pendingChangeRequest({
...mockChangeRequest(featureName, state),
...overrides,
});
changeRequest({ ...mockChangeRequest(featureName, state), ...overrides });
};
const uiConfig = () => {
testServerRoute(server, '/api/admin/ui-config', {
versionInfo: {
@ -249,6 +255,44 @@ test('should show a reschedule dialog when change request is scheduled and updat
await screen.findByRole('dialog', { name: 'Update schedule' });
});
test('should be allowed to apply your own change request if it is approved', async () => {
setupChangeRequest(featureName, 'Approved', {
createdBy: {
id: 17,
imageUrl:
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
},
});
render(<Component />, {
route: '/projects/default/change-requests/1',
permissions: [
{
permission: APPLY_CHANGE_REQUEST,
project: 'default',
environment: 'production',
},
],
});
const applyOrScheduleButton = await screen.findByText(
'Apply or schedule changes',
);
await waitFor(() => expect(applyOrScheduleButton).toBeEnabled(), {
timeout: 3000,
});
fireEvent.click(applyOrScheduleButton);
const scheduleChangesButton = await screen.findByRole('menuitem', {
name: 'Schedule changes',
});
fireEvent.click(scheduleChangesButton);
await screen.findByRole('dialog', { name: 'Schedule changes' });
});
test('should show an apply dialog when change request is scheduled and apply is selected', async () => {
setupChangeRequest(featureName, 'Scheduled');

View File

@ -1,31 +1,49 @@
import { FC } from 'react';
import { FC, useContext } from 'react';
import CheckBox from '@mui/icons-material/Check';
import Clear from '@mui/icons-material/Clear';
import { MultiActionButton } from '../MultiActionButton/MultiActionButton';
import { MultiActionButton } from 'component/common/MultiActionButton/MultiActionButton';
import { APPROVE_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import AccessContext from 'contexts/AccessContext';
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
export const ReviewButton: FC<{
disabled: boolean;
onReject: () => void;
onApprove: () => void;
}> = ({ disabled, onReject, onApprove, children }) => (
<MultiActionButton
permission={APPROVE_CHANGE_REQUEST}
disabled={disabled}
actions={[
{
label: 'Approve',
onSelect: onApprove,
icon: <CheckBox fontSize='small' />,
},
{
label: 'Reject',
onSelect: onReject,
icon: <Clear fontSize='small' />,
},
]}
>
{children}
</MultiActionButton>
);
}> = ({ disabled, onReject, onApprove, children }) => {
const { isAdmin } = useContext(AccessContext);
const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id');
const { user } = useAuthUser();
const { data } = useChangeRequest(projectId, id);
const approverIsCreator = data?.createdBy.id === user?.id;
const disableApprove = disabled || (approverIsCreator && !isAdmin);
return (
<MultiActionButton
permission={APPROVE_CHANGE_REQUEST}
disabled={disableApprove}
actions={[
{
label: 'Approve',
onSelect: onApprove,
icon: <CheckBox fontSize='small' />,
},
{
label: 'Reject',
onSelect: onReject,
icon: <Clear fontSize='small' />,
},
]}
environmentId={data?.environment}
projectId={projectId}
ariaLabel='review or reject changes'
>
{children}
</MultiActionButton>
);
};

View File

@ -15,8 +15,6 @@ import {
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import AccessContext from 'contexts/AccessContext';
type Action = {
label: string;
@ -28,13 +26,18 @@ export const MultiActionButton: FC<{
disabled: boolean;
actions: Action[];
permission: string;
}> = ({ disabled, children, actions, permission }) => {
const { isAdmin } = useContext(AccessContext);
const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id');
const { user } = useAuthUser();
const { data } = useChangeRequest(projectId, id);
projectId?: string;
environmentId?: string;
ariaLabel?: string;
}> = ({
disabled,
children,
actions,
permission,
projectId,
ariaLabel,
environmentId,
}) => {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLButtonElement>(null);
@ -57,19 +60,17 @@ export const MultiActionButton: FC<{
<React.Fragment>
<PermissionButton
variant='contained'
disabled={
disabled || (data?.createdBy.id === user?.id && !isAdmin)
}
disabled={disabled}
aria-controls={open ? 'review-options-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-label='review changes'
aria-label={ariaLabel}
aria-haspopup='menu'
onClick={onToggle}
ref={anchorRef}
endIcon={<ArrowDropDownIcon />}
permission={permission}
projectId={projectId}
environmentId={data?.environment}
environmentId={environmentId}
>
{children}
</PermissionButton>