diff --git a/packages/core/content-releases/admin/src/components/AddReleaseDialog.tsx b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx similarity index 54% rename from packages/core/content-releases/admin/src/components/AddReleaseDialog.tsx rename to packages/core/content-releases/admin/src/components/ReleaseModal.tsx index ee917bcbed..05596f7626 100644 --- a/packages/core/content-releases/admin/src/components/AddReleaseDialog.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx @@ -7,65 +7,29 @@ import { TextInput, Typography, } from '@strapi/design-system'; -import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin'; import { Formik, Form } from 'formik'; import { useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { RELEASE_SCHEMA } from '../../../shared/validation-schemas'; -import { isAxiosError } from '../services/axios'; -import { useCreateReleaseMutation } from '../services/release'; -interface FormValues { +export interface FormValues { name: string; } -const INITIAL_VALUES = { - name: '', -} satisfies FormValues; - -interface AddReleaseDialogProps { +interface ReleaseModalProps { handleClose: () => void; + handleSubmit: (values: FormValues) => void; + isLoading?: boolean; + initialValues: FormValues; } -export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => { +export const ReleaseModal = ({ + handleClose, + handleSubmit, + initialValues, + isLoading = false, +}: ReleaseModalProps) => { const { formatMessage } = useIntl(); - const toggleNotification = useNotification(); - const { formatAPIError } = useAPIErrorHandler(); - const { push } = useHistory(); - - const [createRelease, { isLoading }] = useCreateReleaseMutation(); - - const handleSubmit = async (values: FormValues) => { - const response = await createRelease({ - name: values.name, - }); - - if ('data' in response) { - // When the response returns an object with 'data', handle success - toggleNotification({ - type: 'success', - message: formatMessage({ - id: 'content-releases.modal.release-created-notification-success', - defaultMessage: 'Release created.', - }), - }); - - push(`/plugins/content-releases/${response.data.data.id}`); - } else if (isAxiosError(response.error)) { - // When the response returns an object with 'error', handle axios error - toggleNotification({ - type: 'warning', - message: formatAPIError(response.error), - }); - } else { - // Otherwise, the response returns an object with 'error', handle a generic error - toggleNotification({ - type: 'warning', - message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }), - }); - } - }; return ( @@ -80,7 +44,7 @@ export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => { {({ values, errors, handleChange }) => ( diff --git a/packages/core/content-releases/admin/src/components/tests/AddReleaseDialog.test.tsx b/packages/core/content-releases/admin/src/components/tests/AddReleaseDialog.test.tsx deleted file mode 100644 index 5fc73f31d2..0000000000 --- a/packages/core/content-releases/admin/src/components/tests/AddReleaseDialog.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { within } from '@testing-library/react'; -import { render, screen } from '@tests/utils'; - -import { AddReleaseDialog } from '../AddReleaseDialog'; - -describe('AddReleaseDialog', () => { - it('renders correctly the dialog content', async () => { - const handleCloseMocked = jest.fn(); - const { user } = render(); - const dialogContainer = screen.getByRole('dialog'); - const dialogCancelButton = within(dialogContainer).getByRole('button', { - name: /cancel/i, - }); - expect(dialogCancelButton).toBeInTheDocument(); - await user.click(dialogCancelButton); - expect(handleCloseMocked).toHaveBeenCalledTimes(1); - - // enable the submit button when there is content inside the input - const dialogContinueButton = within(dialogContainer).getByRole('button', { - name: /continue/i, - }); - const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i }); - await user.type(inputElement, 'new release'); - expect(dialogContinueButton).toBeEnabled(); - }); -}); diff --git a/packages/core/content-releases/admin/src/components/tests/ReleaseModal.test.tsx b/packages/core/content-releases/admin/src/components/tests/ReleaseModal.test.tsx new file mode 100644 index 0000000000..ca4c74f34e --- /dev/null +++ b/packages/core/content-releases/admin/src/components/tests/ReleaseModal.test.tsx @@ -0,0 +1,58 @@ +import { within } from '@testing-library/react'; +import { render, screen } from '@tests/utils'; + +import { ReleaseModal } from '../ReleaseModal'; + +describe('ReleaseModal', () => { + it('renders correctly the dialog content on create', async () => { + const handleCloseMocked = jest.fn(); + const { user } = render( + + ); + const dialogContainer = screen.getByRole('dialog'); + const dialogCancelButton = within(dialogContainer).getByRole('button', { + name: /cancel/i, + }); + expect(dialogCancelButton).toBeInTheDocument(); + await user.click(dialogCancelButton); + expect(handleCloseMocked).toHaveBeenCalledTimes(1); + + // the initial field value is empty + const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i }); + expect(inputElement).toHaveValue(''); + + // enable the submit button when there is content inside the input + const dialogContinueButton = within(dialogContainer).getByRole('button', { + name: /continue/i, + }); + await user.type(inputElement, 'new release'); + expect(dialogContinueButton).toBeEnabled(); + }); + it('renders correctly the dialog content on update', async () => { + const handleCloseMocked = jest.fn(); + render( + + ); + const dialogContainer = screen.getByRole('dialog'); + + // the initial field value is the title + const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i }); + expect(inputElement).toHaveValue('title'); + + // enable the submit button when there is content inside the input + const dialogContinueButton = within(dialogContainer).getByRole('button', { + name: /continue/i, + }); + expect(dialogContinueButton).toBeEnabled(); + }); +}); diff --git a/packages/core/content-releases/admin/src/constants.ts b/packages/core/content-releases/admin/src/constants.ts index 455d334cf7..894e8cec45 100644 --- a/packages/core/content-releases/admin/src/constants.ts +++ b/packages/core/content-releases/admin/src/constants.ts @@ -1,20 +1,62 @@ -export const PERMISSIONS = { +import { Permission } from '@strapi/helper-plugin'; + +interface PermissionMap { + main: Permission[]; + create: Permission[]; + update: Permission[]; + delete: Permission[]; + createAction: 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: {}, }, ], }; diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 7a460127ab..d935f63add 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -12,12 +12,16 @@ import { Popover, Typography, } from '@strapi/design-system'; -import { CheckPermissions } from '@strapi/helper-plugin'; +import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin'; import { ArrowLeft, EmptyDocuments, More, Pencil, Trash } from '@strapi/icons'; import { useIntl } from 'react-intl'; +import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { ReleaseModal, FormValues } from '../components/ReleaseModal'; import { PERMISSIONS } from '../constants'; +import { isAxiosError } from '../services/axios'; +import { useUpdateReleaseMutation } from '../services/release'; const PopoverButton = styled(Flex)` align-self: stretch; @@ -46,10 +50,14 @@ const ReleaseInfoWrapper = styled(Flex)` `; const ReleaseDetailsPage = () => { + const { releaseId } = useParams<{ releaseId: string }>(); + const [releaseModalShown, setReleaseModalShown] = React.useState(false); const [isPopoverVisible, setIsPopoverVisible] = React.useState(false); const moreButtonRef = React.useRef(null!); const { formatMessage } = useIntl(); - // TODO: get the title from the API + const toggleNotification = useNotification(); + const { formatAPIError } = useAPIErrorHandler(); + // TODO: get title from the API const title = 'Release title'; const totalEntries = 0; // TODO: replace it with the total number of entries @@ -60,6 +68,47 @@ const ReleaseDetailsPage = () => { setIsPopoverVisible((prev) => !prev); }; + const toggleEditReleaseModal = () => { + setReleaseModalShown((prev) => !prev); + }; + + const openReleaseModal = () => { + toggleEditReleaseModal(); + handleTogglePopover(); + }; + + const [updateRelease, { isLoading }] = useUpdateReleaseMutation(); + + const handleEditRelease = async (values: FormValues) => { + const response = await updateRelease({ + id: releaseId, + name: values.name, + }); + if ('data' in response) { + // When the response returns an object with 'data', handle success + toggleNotification({ + type: 'success', + message: formatMessage({ + id: 'content-releases.modal.release-updated-notification-success', + defaultMessage: 'Release updated.', + }), + }); + } else if (isAxiosError(response.error)) { + // When the response returns an object with 'error', handle axios error + toggleNotification({ + type: 'warning', + message: formatAPIError(response.error), + }); + } else { + // Otherwise, the response returns an object with 'error', handle a generic error + toggleNotification({ + type: 'warning', + message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }), + }); + } + toggleEditReleaseModal(); + }; + return (
{ minWidth="242px" > + + + + + {formatMessage({ + id: 'content-releases.header.actions.edit', + defaultMessage: 'Edit', + })} + + + + { alignItems="center" gap={2} as="button" - borderRadius="4px" - > - - - {formatMessage({ - id: 'content-releases.header.actions.edit', - defaultMessage: 'Edit', - })} - - - @@ -187,6 +240,14 @@ const ReleaseDetailsPage = () => { icon={} /> + {releaseModalShown && ( + + )}
); }; diff --git a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx index 025028c576..889842ba09 100644 --- a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx @@ -25,15 +25,23 @@ import { PageSizeURLQuery, PaginationURLQuery, useQueryParams, + useAPIErrorHandler, + useNotification, } from '@strapi/helper-plugin'; import { EmptyDocuments, Plus } from '@strapi/icons'; import { useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { GetReleases } from '../../../shared/contracts/releases'; -import { AddReleaseDialog } from '../components/AddReleaseDialog'; +import { ReleaseModal, FormValues } from '../components/ReleaseModal'; import { PERMISSIONS } from '../constants'; -import { useGetReleasesQuery, GetReleasesQueryParams } from '../services/release'; +import { isAxiosError } from '../services/axios'; +import { + useGetReleasesQuery, + GetReleasesQueryParams, + useCreateReleaseMutation, +} from '../services/release'; /* ------------------------------------------------------------------------------------------------- * ReleasesLayout @@ -164,21 +172,29 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases /* ------------------------------------------------------------------------------------------------- * ReleasesPage * -----------------------------------------------------------------------------------------------*/ +const INITIAL_FORM_VALUES = { + name: '', +} satisfies FormValues; + const ReleasesPage = () => { - const [addReleaseDialogIsShown, setAddReleaseDialogIsShown] = React.useState(false); + const [releaseModalShown, setReleaseModalShown] = React.useState(false); + const toggleNotification = useNotification(); const { formatMessage } = useIntl(); + const { push } = useHistory(); + const { formatAPIError } = useAPIErrorHandler(); const [{ query }, setQuery] = useQueryParams(); const response = useGetReleasesQuery(query); + const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation(); const { isLoading, isSuccess, isError } = response; - const toggleAddReleaseDialog = () => { - setAddReleaseDialogIsShown((prev) => !prev); + const toggleAddReleaseModal = () => { + setReleaseModalShown((prev) => !prev); }; if (isLoading) { return ( - + @@ -203,8 +219,38 @@ const ReleasesPage = () => { const activeTab = response?.currentData?.meta?.activeTab || 'pending'; + const handleAddRelease = async (values: FormValues) => { + const response = await createRelease({ + name: values.name, + }); + if ('data' in response) { + // When the response returns an object with 'data', handle success + toggleNotification({ + type: 'success', + message: formatMessage({ + id: 'content-releases.modal.release-created-notification-success', + defaultMessage: 'Release created.', + }), + }); + + push(`/plugins/content-releases/${response.data.data.id}`); + } else if (isAxiosError(response.error)) { + // When the response returns an object with 'error', handle axios error + toggleNotification({ + type: 'warning', + message: formatAPIError(response.error), + }); + } else { + // Otherwise, the response returns an object with 'error', handle a generic error + toggleNotification({ + type: 'warning', + message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }), + }); + } + }; + return ( - + <> { )} - {addReleaseDialogIsShown && } + {releaseModalShown && ( + + )} ); }; diff --git a/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx b/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx index 1c498cf3db..0c966c9be8 100644 --- a/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx +++ b/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx @@ -1,9 +1,36 @@ -import { render, screen } from '@tests/utils'; +import { render, screen, server } from '@tests/utils'; +import { rest } from 'msw'; import { ReleaseDetailsPage } from '../ReleaseDetailsPage'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn().mockImplementation(() => ({ id: '1' })), +})); + +jest.mock('@strapi/helper-plugin', () => ({ + ...jest.requireActual('@strapi/helper-plugin'), + // eslint-disable-next-line + CheckPermissions: ({ children }: { children: JSX.Element}) =>
{children}
+})); + describe('Release details page', () => { it('renders correctly the heading content', async () => { + server.use( + rest.put('/content-releases/1', (req, res, ctx) => + res( + ctx.json({ + data: { + id: 2, + name: 'Release title focus', + releasedAt: null, + createdAt: '2023-11-30T16:02:40.908Z', + updatedAt: '2023-12-01T11:12:04.441Z', + }, + }) + ) + ) + ); const { user } = render(); expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Release title'); // if there are 0 entries diff --git a/packages/core/content-releases/admin/src/services/release.ts b/packages/core/content-releases/admin/src/services/release.ts index 6fd719d01f..7213c9b187 100644 --- a/packages/core/content-releases/admin/src/services/release.ts +++ b/packages/core/content-releases/admin/src/services/release.ts @@ -5,9 +5,9 @@ import { pluginId } from '../pluginId'; import { axiosBaseQuery } from './axios'; -import type { CreateRelease, GetReleases } from '../../../shared/contracts/releases'; +import type { CreateRelease, GetReleases, UpdateRelease } from '../../../shared/contracts/releases'; -interface GetReleasesQueryParams { +export interface GetReleasesQueryParams { page?: number; pageSize?: number; filters?: { @@ -104,6 +104,19 @@ const releaseApi = createApi({ }, invalidatesTags: ['Releases'], }), + updateRelease: build.mutation< + void, + UpdateRelease.Request['params'] & UpdateRelease.Request['body'] + >({ + query({ id, ...data }) { + return { + url: `/content-releases/${id}`, + method: 'PUT', + data, + }; + }, + invalidatesTags: ['Releases'], + }), createReleaseAction: build.mutation< CreateReleaseAction.Response, CreateReleaseAction.Request @@ -126,6 +139,7 @@ const { useGetReleasesForEntryQuery, useCreateReleaseMutation, useCreateReleaseActionMutation, + useUpdateReleaseMutation, } = releaseApi; export { @@ -133,7 +147,6 @@ export { useGetReleasesForEntryQuery, useCreateReleaseMutation, useCreateReleaseActionMutation, + useUpdateReleaseMutation, releaseApi, }; - -export type { GetReleasesQueryParams }; diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json index 5f5c989c96..5210aa1c39 100644 --- a/packages/core/content-releases/admin/src/translations/en.json +++ b/packages/core/content-releases/admin/src/translations/en.json @@ -17,8 +17,9 @@ "header.actions.edit": "Edit", "header.actions.delete": "Delete", "header.actions.created": "Created", - "header.actions.created.description": "{number, plural, =0 {# days} one {# day} other {# days}} ago by {user}", + "header.actions.created.description": "{number, plural, =0 {# days} one {# day} other {# days}} ago by {createdBy}", "modal.release-created-notification-success": "Release created", + "modal.release-updated-notification-success": "Release updated", "modal.add-release-title": "New Release", "modal.form.input.label.release-name": "Name", "modal.form.button.submit": "Continue",