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 { useCreateReleaseActionMutation, useGetReleasesForEntryQuery } from '../services/release';
import { ReleaseActionMenu } from './ReleaseActionMenu';
import { ReleaseActionOptions } from './ReleaseActionOptions';
/* -------------------------------------------------------------------------------------------------
* AddActionToReleaseModal
* -----------------------------------------------------------------------------------------------*/
const RELEASE_ACTION_FORM_SCHEMA = yup.object().shape({
type: yup.string().oneOf(['publish', 'unpublish']).required(),
releaseId: yup.string().required(),
@ -199,8 +204,12 @@ const AddActionToReleaseModal = ({
);
};
/* -------------------------------------------------------------------------------------------------
* CMReleasesContainer
* -----------------------------------------------------------------------------------------------*/
export const CMReleasesContainer = () => {
const [showModal, setShowModal] = React.useState(false);
const [isModalOpen, setIsModalOpen] = React.useState(false);
const { formatMessage } = useIntl();
const {
isCreatingEntry,
@ -237,7 +246,7 @@ export const CMReleasesContainer = () => {
return null;
}
const toggleAddActionToReleaseModal = () => setShowModal((prev) => !prev);
const toggleModal = () => setIsModalOpen((prev) => !prev);
const getReleaseColorVariant = (
actionType: 'publish' | 'unpublish',
@ -306,11 +315,12 @@ export const CMReleasesContainer = () => {
)}
</Typography>
</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">
{release.name}
</Typography>
</Box>
<ReleaseActionMenu releaseId={release.id} actionId={release.action.id} />
</Flex>
</Flex>
);
})}
@ -322,7 +332,7 @@ export const CMReleasesContainer = () => {
color="neutral700"
variant="tertiary"
startIcon={<Plus />}
onClick={toggleAddActionToReleaseModal}
onClick={toggleModal}
>
{formatMessage({
id: 'content-releases.content-manager-edit-view.add-to-release',
@ -331,9 +341,9 @@ export const CMReleasesContainer = () => {
</Button>
</CheckPermissions>
</Flex>
{showModal && (
{isModalOpen && (
<AddActionToReleaseModal
handleClose={toggleAddActionToReleaseModal}
handleClose={toggleModal}
contentTypeUid={contentType.uid}
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 />);
const informationBox = await screen.findByRole('complementary', { name: 'Releases' });
expect(within(informationBox).getByText('release1')).toBeInTheDocument();
expect(within(informationBox).getByText('release2')).toBeInTheDocument();
const release1 = await within(informationBox).findByText('release1');
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 {
main: Permission[];
create: Permission[];
update: Permission[];
delete: Permission[];
createAction: Permission[];
deleteAction: Permission[];
}
export const PERMISSIONS: PermissionMap = {
main: [
{
id: 293,
action: 'plugin::content-releases.read',
subject: null,
conditions: [],
actionParameters: [],
properties: {},
},
],
create: [
{
id: 294,
action: 'plugin::content-releases.create',
subject: null,
conditions: [],
actionParameters: [],
properties: {},
},
],
update: [
{
id: 295,
action: 'plugin::content-releases.update',
subject: null,
conditions: [],
actionParameters: [],
properties: {},
},
],
delete: [
{
id: 296,
action: 'plugin::content-releases.delete',
subject: null,
conditions: [],
actionParameters: [],
properties: {},
},
],
createAction: [
{
id: 297,
action: 'plugin::content-releases.create-action',
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 { CreateReleaseAction } from '../../../shared/contracts/release-actions';
import {
CreateReleaseAction,
DeleteReleaseAction,
} from '../../../shared/contracts/release-actions';
import { pluginId } from '../pluginId';
import { axiosBaseQuery } from './axios';
@ -202,6 +205,21 @@ const releaseApi = createApi({
{ 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,
useUpdateReleaseMutation,
useUpdateReleaseActionMutation,
useDeleteReleaseActionMutation,
} = releaseApi;
export {
@ -226,5 +245,6 @@ export {
useCreateReleaseActionMutation,
useUpdateReleaseMutation,
useUpdateReleaseActionMutation,
useDeleteReleaseActionMutation,
releaseApi,
};

View File

@ -7,6 +7,9 @@
"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.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",
"plugin.name": "Releases",
"pages.Releases.title": "Releases",

View File

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

View File

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