feat(content-releases): add delete release action button (#19047)

This commit is contained in:
markkaylor 2023-12-14 12:38:05 +01:00 committed by GitHub
parent 849c8126fd
commit 93969787db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 35 deletions

View File

@ -32,8 +32,13 @@ import { GetContentTypeEntryReleases } from '../../../shared/contracts/releases'
import { PERMISSIONS } from '../constants'; import { PERMISSIONS } from '../constants';
import { useCreateReleaseActionMutation, useGetReleasesForEntryQuery } from '../services/release'; import { useCreateReleaseActionMutation, useGetReleasesForEntryQuery } from '../services/release';
import { ReleaseActionMenu } from './ReleaseActionMenu';
import { ReleaseActionOptions } from './ReleaseActionOptions'; import { ReleaseActionOptions } from './ReleaseActionOptions';
/* -------------------------------------------------------------------------------------------------
* AddActionToReleaseModal
* -----------------------------------------------------------------------------------------------*/
const RELEASE_ACTION_FORM_SCHEMA = yup.object().shape({ const RELEASE_ACTION_FORM_SCHEMA = yup.object().shape({
type: yup.string().oneOf(['publish', 'unpublish']).required(), type: yup.string().oneOf(['publish', 'unpublish']).required(),
releaseId: yup.string().required(), releaseId: yup.string().required(),
@ -199,8 +204,12 @@ const AddActionToReleaseModal = ({
); );
}; };
/* -------------------------------------------------------------------------------------------------
* CMReleasesContainer
* -----------------------------------------------------------------------------------------------*/
export const CMReleasesContainer = () => { export const CMReleasesContainer = () => {
const [showModal, setShowModal] = React.useState(false); const [isModalOpen, setIsModalOpen] = React.useState(false);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { const {
isCreatingEntry, isCreatingEntry,
@ -237,7 +246,7 @@ export const CMReleasesContainer = () => {
return null; return null;
} }
const toggleAddActionToReleaseModal = () => setShowModal((prev) => !prev); const toggleModal = () => setIsModalOpen((prev) => !prev);
const getReleaseColorVariant = ( const getReleaseColorVariant = (
actionType: 'publish' | 'unpublish', actionType: 'publish' | 'unpublish',
@ -306,11 +315,12 @@ export const CMReleasesContainer = () => {
)} )}
</Typography> </Typography>
</Box> </Box>
<Box padding={4}> <Flex padding={4} direction="column" gap={3} width="100%" alignItems="flex-start">
<Typography fontSize={2} fontWeight="bold" variant="omega" textColor="neutral700"> <Typography fontSize={2} fontWeight="bold" variant="omega" textColor="neutral700">
{release.name} {release.name}
</Typography> </Typography>
</Box> <ReleaseActionMenu releaseId={release.id} actionId={release.action.id} />
</Flex>
</Flex> </Flex>
); );
})} })}
@ -322,7 +332,7 @@ export const CMReleasesContainer = () => {
color="neutral700" color="neutral700"
variant="tertiary" variant="tertiary"
startIcon={<Plus />} startIcon={<Plus />}
onClick={toggleAddActionToReleaseModal} onClick={toggleModal}
> >
{formatMessage({ {formatMessage({
id: 'content-releases.content-manager-edit-view.add-to-release', id: 'content-releases.content-manager-edit-view.add-to-release',
@ -331,9 +341,9 @@ export const CMReleasesContainer = () => {
</Button> </Button>
</CheckPermissions> </CheckPermissions>
</Flex> </Flex>
{showModal && ( {isModalOpen && (
<AddActionToReleaseModal <AddActionToReleaseModal
handleClose={toggleAddActionToReleaseModal} handleClose={toggleModal}
contentTypeUid={contentType.uid} contentTypeUid={contentType.uid}
entryId={params.id} entryId={params.id}
/> />

View File

@ -0,0 +1,125 @@
import { Flex, IconButton, Typography } from '@strapi/design-system';
import { Menu } from '@strapi/design-system/v2';
import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
import { Cross, More } from '@strapi/icons';
import { isAxiosError } from 'axios';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { DeleteReleaseAction } from '../../../shared/contracts/release-actions';
import { PERMISSIONS } from '../constants';
import { useDeleteReleaseActionMutation } from '../services/release';
const StyledMenuItem = styled(Menu.Item)`
&:hover {
background: transparent;
}
svg {
path {
fill: ${({ theme }) => theme.colors.danger600};
}
}
&:hover {
svg {
path {
fill: ${({ theme }) => theme.colors.danger600};
}
}
}
`;
const StyledCross = styled(Cross)`
padding: ${({ theme }) => theme.spaces[1]};
`;
interface ReleaseActionMenuProps {
releaseId: DeleteReleaseAction.Request['params']['releaseId'];
actionId: DeleteReleaseAction.Request['params']['actionId'];
}
export const ReleaseActionMenu = ({ releaseId, actionId }: ReleaseActionMenuProps) => {
const { formatMessage } = useIntl();
const toggleNotification = useNotification();
const { formatAPIError } = useAPIErrorHandler();
const [deleteReleaseAction] = useDeleteReleaseActionMutation();
const handleDeleteAction = async () => {
const response = await deleteReleaseAction({
params: { releaseId, actionId },
});
if ('data' in response) {
// Handle success
toggleNotification({
type: 'success',
message: formatMessage({
id: 'content-releases.content-manager-edit-view.remove-from-release.notification.success',
defaultMessage: 'Entry removed from release',
}),
});
return;
}
if ('error' in response) {
if (isAxiosError(response.error)) {
// Handle axios error
toggleNotification({
type: 'warning',
message: formatAPIError(response.error),
});
} else {
// Handle generic error
toggleNotification({
type: 'warning',
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
});
}
}
};
return (
// A user can access the dropdown if they have permissions to delete a release-action OR update a release
<CheckPermissions permissions={[...PERMISSIONS.deleteAction, ...PERMISSIONS.update]}>
<Menu.Root>
{/*
TODO Fix in the DS
- as={IconButton} has TS error: Property 'icon' does not exist on type 'IntrinsicAttributes & TriggerProps & RefAttributes<HTMLButtonElement>'
- The Icon doesn't actually show unless you hack it with some padding...and it's still a little strange
*/}
<Menu.Trigger
as={IconButton}
paddingLeft={2}
paddingRight={2}
aria-label={formatMessage({
id: 'content-releases.content-manager-edit-view.release-action-menu',
defaultMessage: 'Release action options',
})}
// @ts-expect-error See above
icon={<More />}
/>
{/*
TODO: Using Menu instead of SimpleMenu mainly because there is no positioning provided from the DS,
Refactor this once fixed in the DS
*/}
<Menu.Content top={1}>
<CheckPermissions permissions={PERMISSIONS.deleteAction}>
<StyledMenuItem color="danger600" onSelect={handleDeleteAction}>
<Flex gap={2}>
<StyledCross />
<Typography variant="omega">
{formatMessage({
id: 'content-releases.content-manager-edit-view.remove-from-release',
defaultMessage: 'Remove from release',
})}
</Typography>
</Flex>
</StyledMenuItem>
</CheckPermissions>
</Menu.Content>
</Menu.Root>
</CheckPermissions>
);
};

View File

@ -132,7 +132,9 @@ describe('CMReleasesContainer', () => {
render(<CMReleasesContainer />); render(<CMReleasesContainer />);
const informationBox = await screen.findByRole('complementary', { name: 'Releases' }); const informationBox = await screen.findByRole('complementary', { name: 'Releases' });
expect(within(informationBox).getByText('release1')).toBeInTheDocument(); const release1 = await within(informationBox).findByText('release1');
expect(within(informationBox).getByText('release2')).toBeInTheDocument(); const release2 = await within(informationBox).findByText('release2');
expect(release1).toBeInTheDocument();
expect(release2).toBeInTheDocument();
}); });
}); });

View File

@ -0,0 +1,21 @@
import { render, screen } from '@tests/utils';
import { ReleaseActionMenu } from '../ReleaseActionMenu';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
// eslint-disable-next-line
CheckPermissions: ({ children }: { children: JSX.Element }) => <div>{children}</div>,
}));
describe('ReleaseActionMenu', () => {
it('should render the menu with its options', async () => {
const { user } = render(<ReleaseActionMenu releaseId="1" actionId="1" />);
const menuTrigger = screen.getByRole('button', { name: 'Release action options' });
expect(menuTrigger).toBeInTheDocument();
await user.click(menuTrigger);
expect(screen.getByRole('menuitem', { name: 'Remove from release' })).toBeInTheDocument();
});
});

View File

@ -1,62 +1,50 @@
import { Permission } from '@strapi/helper-plugin'; import { Permission as StrapiPermission } from '@strapi/helper-plugin';
type Permission = Pick<StrapiPermission, 'action' | 'subject'>;
interface PermissionMap { interface PermissionMap {
main: Permission[]; main: Permission[];
create: Permission[]; create: Permission[];
update: Permission[]; update: Permission[];
delete: Permission[]; delete: Permission[];
createAction: Permission[]; createAction: Permission[];
deleteAction: Permission[];
} }
export const PERMISSIONS: PermissionMap = { export const PERMISSIONS: PermissionMap = {
main: [ main: [
{ {
id: 293,
action: 'plugin::content-releases.read', action: 'plugin::content-releases.read',
subject: null, subject: null,
conditions: [],
actionParameters: [],
properties: {},
}, },
], ],
create: [ create: [
{ {
id: 294,
action: 'plugin::content-releases.create', action: 'plugin::content-releases.create',
subject: null, subject: null,
conditions: [],
actionParameters: [],
properties: {},
}, },
], ],
update: [ update: [
{ {
id: 295,
action: 'plugin::content-releases.update', action: 'plugin::content-releases.update',
subject: null, subject: null,
conditions: [],
actionParameters: [],
properties: {},
}, },
], ],
delete: [ delete: [
{ {
id: 296,
action: 'plugin::content-releases.delete', action: 'plugin::content-releases.delete',
subject: null, subject: null,
conditions: [],
actionParameters: [],
properties: {},
}, },
], ],
createAction: [ createAction: [
{ {
id: 297,
action: 'plugin::content-releases.create-action', action: 'plugin::content-releases.create-action',
subject: null, subject: null,
conditions: [], },
actionParameters: [], ],
properties: {}, deleteAction: [
{
action: 'plugin::content-releases.delete-action',
subject: null,
}, },
], ],
}; };

View File

@ -1,6 +1,9 @@
import { createApi } from '@reduxjs/toolkit/query/react'; import { createApi } from '@reduxjs/toolkit/query/react';
import { CreateReleaseAction } from '../../../shared/contracts/release-actions'; import {
CreateReleaseAction,
DeleteReleaseAction,
} from '../../../shared/contracts/release-actions';
import { pluginId } from '../pluginId'; import { pluginId } from '../pluginId';
import { axiosBaseQuery } from './axios'; import { axiosBaseQuery } from './axios';
@ -202,6 +205,21 @@ const releaseApi = createApi({
{ type: 'ReleaseAction', id: arg.params.actionId }, { type: 'ReleaseAction', id: arg.params.actionId },
], ],
}), }),
deleteReleaseAction: build.mutation<
DeleteReleaseAction.Response,
DeleteReleaseAction.Request
>({
query({ params }) {
return {
url: `/content-releases/${params.releaseId}/actions/${params.actionId}`,
method: 'DELETE',
};
},
invalidatesTags: [
{ type: 'Release', id: 'LIST' },
{ type: 'ReleaseAction', id: 'LIST' },
],
}),
}; };
}, },
}); });
@ -215,6 +233,7 @@ const {
useCreateReleaseActionMutation, useCreateReleaseActionMutation,
useUpdateReleaseMutation, useUpdateReleaseMutation,
useUpdateReleaseActionMutation, useUpdateReleaseActionMutation,
useDeleteReleaseActionMutation,
} = releaseApi; } = releaseApi;
export { export {
@ -226,5 +245,6 @@ export {
useCreateReleaseActionMutation, useCreateReleaseActionMutation,
useUpdateReleaseMutation, useUpdateReleaseMutation,
useUpdateReleaseActionMutation, useUpdateReleaseActionMutation,
useDeleteReleaseActionMutation,
releaseApi, releaseApi,
}; };

View File

@ -7,6 +7,9 @@
"content-manager-edit-view.add-to-release": "Add to release", "content-manager-edit-view.add-to-release": "Add to release",
"content-manager-edit-view.add-to-release.notification.success": "Entry added to release", "content-manager-edit-view.add-to-release.notification.success": "Entry added to release",
"content-manager-edit-view.list-releases.title": "{isPublish, select, true {Will be published in} other {Will be unpublished in}}", "content-manager-edit-view.list-releases.title": "{isPublish, select, true {Will be published in} other {Will be unpublished in}}",
"content-manager-edit-view.remove-from-release": "Remove from release",
"content-manager-edit-view.remove-from-release.notification.success": "Entry removed from release",
"content-manager-edit-view.release-action-menu": "Release action options",
"content-manager.notification.entry-error": "Failed to get entry data", "content-manager.notification.entry-error": "Failed to get entry data",
"plugin.name": "Releases", "plugin.name": "Releases",
"pages.Releases.title": "Releases", "pages.Releases.title": "Releases",

View File

@ -116,9 +116,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
return { return {
...release, ...release,
action: { action: actionForEntry
type: actionForEntry.type,
},
}; };
} }

View File

@ -23,7 +23,7 @@ export interface ReleaseDataResponse extends Omit<Release, 'actions'> {
} }
export interface ReleaseForContentTypeEntryDataResponse extends Omit<Release, 'actions'> { export interface ReleaseForContentTypeEntryDataResponse extends Omit<Release, 'actions'> {
action: { type: ReleaseAction['type'] }; action: ReleaseAction;
} }
/** /**