mirror of
https://github.com/strapi/strapi.git
synced 2025-10-30 17:37:26 +00:00
feat(content-releases): add delete release action button (#19047)
This commit is contained in:
parent
849c8126fd
commit
93969787db
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -116,9 +116,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...release,
|
...release,
|
||||||
action: {
|
action: actionForEntry
|
||||||
type: actionForEntry.type,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user