mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +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
	 markkaylor
						markkaylor