From 4d6d68b218ba9120b9de23a74a16e4445b8eb1d6 Mon Sep 17 00:00:00 2001 From: Mark Kaylor Date: Tue, 19 Dec 2023 14:44:40 +0100 Subject: [PATCH 1/9] feat(content-releases): delete release action on release page --- .../admin/src/components/ReleaseActionMenu.tsx | 6 +++--- .../admin/src/pages/ReleaseDetailsPage.tsx | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx b/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx index 46ac52979a..501f3db39e 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx @@ -12,7 +12,7 @@ import { useDeleteReleaseActionMutation } from '../services/release'; const StyledMenuItem = styled(Menu.Item)` &:hover { - background: transparent; + background: ${({ theme }) => theme.colors.danger100}; } svg { @@ -100,11 +100,11 @@ export const ReleaseActionMenu = ({ releaseId, actionId }: ReleaseActionMenuProp // @ts-expect-error See above icon={} /> - {/* + {/* TODO: Using Menu instead of SimpleMenu mainly because there is no positioning provided from the DS, Refactor this once fixed in the DS */} - + diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 030eb48db5..a49b950751 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -33,6 +33,7 @@ import { useIntl } from 'react-intl'; import { useParams, useHistory } from 'react-router-dom'; import styled from 'styled-components'; +import { ReleaseActionMenu } from '../components/ReleaseActionMenu'; import { ReleaseActionOptions } from '../components/ReleaseActionOptions'; import { ReleaseModal, FormValues } from '../components/ReleaseModal'; import { PERMISSIONS } from '../constants'; @@ -405,9 +406,9 @@ const ReleaseDetailsBody = () => { ({ - ...item, - id: Number(item.entry.id), + rows={releaseActions.map((action) => ({ + ...action, + id: Number(action.entry.id), }))} colCount={releaseActions.length} isLoading={isLoading} @@ -447,6 +448,7 @@ const ReleaseDetailsBody = () => { })} name="action" /> + @@ -486,6 +488,11 @@ const ReleaseDetailsBody = () => { /> )} + + + + + ))} From 0e1d097bba977f78d15ad58af8c586b3eaf4dd80 Mon Sep 17 00:00:00 2001 From: Convly Date: Wed, 3 Jan 2024 14:55:00 +0100 Subject: [PATCH 2/9] fix: avoid undefined controllers in telemetry checks --- packages/core/strapi/src/Strapi.ts | 3 ++- packages/core/strapi/src/factories.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/strapi/src/Strapi.ts b/packages/core/strapi/src/Strapi.ts index d1f7e3078d..af5cda6dfe 100644 --- a/packages/core/strapi/src/Strapi.ts +++ b/packages/core/strapi/src/Strapi.ts @@ -381,7 +381,8 @@ class Strapi implements StrapiI { numberOfComponents: _.size(this.components), numberOfDynamicZones: getNumberOfDynamicZones(), numberOfCustomControllers: Object.values(this.controllers).filter( - factories.isCustomController + // TODO: Fix this at the content API loader level to prevent future types issues + (controller) => controller !== undefined && factories.isCustomController(controller) ).length, environment: this.config.environment, // TODO: to add back diff --git a/packages/core/strapi/src/factories.ts b/packages/core/strapi/src/factories.ts index 798886cd55..76e6aa260d 100644 --- a/packages/core/strapi/src/factories.ts +++ b/packages/core/strapi/src/factories.ts @@ -43,8 +43,8 @@ const createCoreController = < Object.setPrototypeOf(userCtrl, baseController); - const isCustomController = typeof cfg !== 'undefined'; - if (isCustomController) { + const isCustom = typeof cfg !== 'undefined'; + if (isCustom) { Object.defineProperty(userCtrl, symbols.CustomController, { writable: false, configurable: false, From 29255bc953ad5e2ca1458e157bdf55819b15696f Mon Sep 17 00:00:00 2001 From: markkaylor Date: Thu, 4 Jan 2024 10:18:21 +0100 Subject: [PATCH 3/9] feat(content-releases): group release actions by property (#19097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(content-releases): group release actions by property * fix: remi feedback * feat: sort findMany actions * chore(content-releases): group by UI for content releases (#19133) * feat(content-releases): group by UI for content releases * apply remi's and josh's feedback * remove unnecessary types --------- Co-authored-by: Fernando Chávez --- .../src/components/CMReleasesContainer.tsx | 3 + .../admin/src/pages/ReleaseDetailsPage.tsx | 258 +++++++++++------- .../pages/tests/ReleaseDetailsPage.test.tsx | 41 ++- .../pages/tests/mockReleaseDetailsPageData.ts | 111 ++++++-- .../admin/src/services/release.ts | 21 +- .../admin/src/translations/en.json | 3 +- packages/core/content-releases/package.json | 1 + .../content-types/release-action/schema.ts | 3 + .../__tests__/release-action.test.ts | 94 ------- .../server/src/controllers/release-action.ts | 44 +-- .../src/services/__tests__/release.test.ts | 88 ++++++ .../server/src/services/release.ts | 96 +++++-- .../shared/contracts/release-actions.ts | 14 +- yarn.lock | 1 + 14 files changed, 480 insertions(+), 298 deletions(-) diff --git a/packages/core/content-releases/admin/src/components/CMReleasesContainer.tsx b/packages/core/content-releases/admin/src/components/CMReleasesContainer.tsx index 5bd5f78131..35868ca6de 100644 --- a/packages/core/content-releases/admin/src/components/CMReleasesContainer.tsx +++ b/packages/core/content-releases/admin/src/components/CMReleasesContainer.tsx @@ -98,6 +98,7 @@ const AddActionToReleaseModal = ({ const { formatMessage } = useIntl(); const toggleNotification = useNotification(); const { formatAPIError } = useAPIErrorHandler(); + const { modifiedData } = useCMEditViewDataManager(); // Get all 'pending' releases that do not have the entry attached const response = useGetReleasesForEntryQuery({ @@ -110,9 +111,11 @@ const AddActionToReleaseModal = ({ const [createReleaseAction, { isLoading }] = useCreateReleaseActionMutation(); const handleSubmit = async (values: FormValues) => { + const locale = modifiedData.locale as string | undefined; const releaseActionEntry = { contentType: contentTypeUid, id: entryId, + locale, }; const response = await createReleaseAction({ body: { type: values.type, entry: releaseActionEntry }, diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 6e88144cf0..9b766b7c20 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -12,6 +12,9 @@ import { Tr, Td, Typography, + Badge, + SingleSelect, + SingleSelectOption, } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; import { @@ -48,7 +51,10 @@ import { useDeleteReleaseMutation, } from '../services/release'; -import type { ReleaseAction } from '../../../shared/contracts/release-actions'; +import type { + ReleaseAction, + ReleaseActionGroupBy, +} from '../../../shared/contracts/release-actions'; /* ------------------------------------------------------------------------------------------------- * ReleaseDetailsLayout @@ -321,13 +327,28 @@ export const ReleaseDetailsLayout = ({ ); }; +const GROUP_BY_OPTIONS = [ + { + label: 'Content Type', + value: 'contentType', + }, + { + label: 'Locale', + value: 'locale', + }, + { + label: 'Action', + value: 'action', + }, +]; + /* ------------------------------------------------------------------------------------------------- * ReleaseDetailsBody * -----------------------------------------------------------------------------------------------*/ const ReleaseDetailsBody = () => { const { formatMessage } = useIntl(); const { releaseId } = useParams<{ releaseId: string }>(); - const [{ query }] = useQueryParams(); + const [{ query }, setQuery] = useQueryParams(); const toggleNotification = useNotification(); const { formatAPIError } = useAPIErrorHandler(); const { @@ -390,7 +411,10 @@ const ReleaseDetailsBody = () => { ); } - if (isError || isReleaseError || !release) { + const releaseActions = data?.data; + const releaseMeta = data?.meta; + + if (isError || isReleaseError || !release || !releaseActions) { const errorsArray = []; if (releaseError) { errorsArray.push({ @@ -414,10 +438,7 @@ const ReleaseDetailsBody = () => { ); } - const releaseActions = data?.data; - const releaseMeta = data?.meta; - - if (!releaseActions || !releaseActions.length) { + if (Object.keys(releaseActions).length === 0) { return ( { return ( - - ({ - ...action, - id: Number(action.entry.id), - }))} - colCount={releaseActions.length} - isLoading={isLoading} - isFetching={isFetching} - > - - - - - - - - - - - {releaseActions.map(({ id, type, entry }) => ( - - - {`${entry.contentType.mainFieldValue || entry.id}`} - - - {`${entry?.locale?.name ? entry.locale.name : '-'}`} - - - {entry.contentType.displayName || ''} - - - {release.releasedAt ? ( - - {formatMessage( - { - id: 'content-releases.page.ReleaseDetails.table.action-published', - defaultMessage: - 'This entry was {isPublish, select, true {published} other {unpublished}}.', - }, - { - isPublish: type === 'publish', - b: (children: React.ReactNode) => ( - {children} - ), - } + + + + formatMessage( + { + id: `pages.ReleaseDetails.groupBy.label}`, + defaultMessage: `Group by {groupBy}`, + }, + { + groupBy: value, + } + ) + } + value={query?.groupBy || 'contentType'} + onChange={(value) => setQuery({ groupBy: value as ReleaseActionGroupBy })} + > + {GROUP_BY_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + {Object.keys(releaseActions).map((key) => ( + + + {key} + + ({ + ...item, + id: Number(item.entry.id), + }))} + colCount={releaseActions[key].length} + isLoading={isLoading} + isFetching={isFetching} + > + + + + + + + + + + {releaseActions[key].map(({ id, type, entry }) => ( + + + {`${ + entry.contentType.mainFieldValue || entry.id + }`} + + + {`${ + entry?.locale?.name ? entry.locale.name : '-' + }`} + + + {entry.contentType.displayName || ''} + + + {release.releasedAt ? ( + + {formatMessage( + { + id: 'content-releases.page.ReleaseDetails.table.action-published', + defaultMessage: + 'This entry was {isPublish, select, true {published} other {unpublished}}.', + }, + { + isPublish: type === 'publish', + b: (children: React.ReactNode) => ( + {children} + ), + } + )} + + ) : ( + handleChangeType(e, id)} + name={`release-action-${id}-type`} + /> )} - - ) : ( - handleChangeType(e, id)} - name={`release-action-${id}-type`} - /> - )} - - - - - - - - ))} - - - + + + + + + + + ))} + + + + + ))} { // should show the entries expect( screen.getByText( - mockReleaseDetailsPageData.withActionsBodyData.data[0].entry.contentType.mainFieldValue + mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.contentType + .mainFieldValue ) ).toBeInTheDocument(); + expect( + screen.getByRole('gridcell', { + name: mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.contentType + .displayName, + }) + ).toBeInTheDocument(); expect( screen.getByText( - mockReleaseDetailsPageData.withActionsBodyData.data[0].entry.contentType.displayName + mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.locale.name ) ).toBeInTheDocument(); - expect( - screen.getByText(mockReleaseDetailsPageData.withActionsBodyData.data[0].entry.locale.name) - ).toBeInTheDocument(); // There is one column with actions and the right one is checked expect(screen.getByRole('radio', { name: 'publish' })).toBeChecked(); @@ -184,4 +188,31 @@ describe('Releases details page', () => { const deleteButton = screen.getByRole('button', { name: 'Delete' }); expect(deleteButton).toBeDisabled(); }); + + it('renders as many tables as there are in the response', async () => { + server.use( + rest.get('/content-releases/:releaseId', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withActionsHeaderData)) + ) + ); + + server.use( + rest.get('/content-releases/:releaseId/actions', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withMultipleActionsBodyData)) + ) + ); + + render(, { + initialEntries: [{ pathname: `/content-releases/1` }], + }); + + const releaseTitle = await screen.findByText( + mockReleaseDetailsPageData.withActionsHeaderData.data.name + ); + expect(releaseTitle).toBeInTheDocument(); + + const tables = screen.getAllByRole('grid'); + + expect(tables).toHaveLength(2); + }); }); diff --git a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts index a0f3e23dd9..122af5159b 100644 --- a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts +++ b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts @@ -93,26 +93,102 @@ const PUBLISHED_RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { * RELEASE_WITH_ACTIONS_BODY_MOCK_DATA * -----------------------------------------------------------------------------------------------*/ const RELEASE_WITH_ACTIONS_BODY_MOCK_DATA = { - data: [ - { - id: 3, - type: 'publish', - contentType: 'api::category.category', - createdAt: '2023-12-05T09:03:57.155Z', - updatedAt: '2023-12-05T09:03:57.155Z', - entry: { - id: 1, - contentType: { - displayName: 'Category', - mainFieldValue: 'cat1', - }, - locale: { - name: 'English (en)', - code: 'en', + data: { + Category: [ + { + id: 3, + type: 'publish', + contentType: 'api::category.category', + createdAt: '2023-12-05T09:03:57.155Z', + updatedAt: '2023-12-05T09:03:57.155Z', + entry: { + id: 1, + contentType: { + displayName: 'Category', + mainFieldValue: 'cat1', + }, + locale: { + name: 'English (en)', + code: 'en', + }, }, }, + ], + }, + meta: { + pagination: { + page: 1, + pageSize: 10, + total: 1, + pageCount: 1, }, - ], + }, +}; + +/* ------------------------------------------------------------------------------------------------- + * RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA + * -----------------------------------------------------------------------------------------------*/ +const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = { + data: { + Category: [ + { + id: 3, + type: 'publish', + contentType: 'api::category.category', + createdAt: '2023-12-05T09:03:57.155Z', + updatedAt: '2023-12-05T09:03:57.155Z', + entry: { + id: 1, + contentType: { + displayName: 'Category', + mainFieldValue: 'cat1', + }, + locale: { + name: 'English (en)', + code: 'en', + }, + }, + }, + { + id: 4, + type: 'publish', + contentType: 'api::category.category', + createdAt: '2023-12-05T09:03:57.155Z', + updatedAt: '2023-12-05T09:03:57.155Z', + entry: { + id: 2, + contentType: { + displayName: 'Category', + mainFieldValue: 'cat1', + }, + locale: { + name: 'English (en)', + code: 'en', + }, + }, + }, + ], + Address: [ + { + id: 5, + type: 'publish', + contentType: 'api::address.address', + createdAt: '2023-12-05T09:03:57.155Z', + updatedAt: '2023-12-05T09:03:57.155Z', + entry: { + id: 1, + contentType: { + displayName: 'Address', + mainFieldValue: 'add1', + }, + locale: { + name: 'English (en)', + code: 'en', + }, + }, + }, + ], + }, meta: { pagination: { page: 1, @@ -128,6 +204,7 @@ const mockReleaseDetailsPageData = { noActionsBodyData: RELEASE_NO_ACTIONS_BODY_MOCK_DATA, withActionsHeaderData: RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA, withActionsBodyData: RELEASE_WITH_ACTIONS_BODY_MOCK_DATA, + withMultipleActionsBodyData: RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA, withActionsAndPublishedHeaderData: PUBLISHED_RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA, } as const; diff --git a/packages/core/content-releases/admin/src/services/release.ts b/packages/core/content-releases/admin/src/services/release.ts index 5a1bce7813..efb4542eb8 100644 --- a/packages/core/content-releases/admin/src/services/release.ts +++ b/packages/core/content-releases/admin/src/services/release.ts @@ -11,6 +11,7 @@ import { axiosBaseQuery } from './axios'; import type { GetReleaseActions, UpdateReleaseAction, + ReleaseActionGroupBy, } from '../../../shared/contracts/release-actions'; import type { CreateRelease, @@ -36,6 +37,7 @@ export interface GetReleasesQueryParams { export interface GetReleaseActionsQueryParams { page?: number; pageSize?: number; + groupBy?: ReleaseActionGroupBy; } type GetReleasesTabResponse = GetReleases.Response & { @@ -133,25 +135,16 @@ const releaseApi = createApi({ GetReleaseActions.Response, GetReleaseActions.Request['params'] & GetReleaseActions.Request['query'] >({ - query({ releaseId, page, pageSize }) { + query({ releaseId, ...params }) { return { url: `/content-releases/${releaseId}/actions`, method: 'GET', config: { - params: { - page, - pageSize, - }, + params, }, }; }, - providesTags: (result, error, arg) => - result - ? [ - ...result.data.map(({ id }) => ({ type: 'ReleaseAction' as const, id })), - { type: 'ReleaseAction', id: 'LIST' }, - ] - : [{ type: 'ReleaseAction', id: 'LIST' }], + providesTags: [{ type: 'ReleaseAction', id: 'LIST' }], }), createRelease: build.mutation({ query(data) { @@ -203,9 +196,7 @@ const releaseApi = createApi({ data: body, }; }, - invalidatesTags: (result, error, arg) => [ - { type: 'ReleaseAction', id: arg.params.actionId }, - ], + invalidatesTags: () => [{ type: 'ReleaseAction', id: 'LIST' }], }), deleteReleaseAction: build.mutation< DeleteReleaseAction.Response, diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json index e3638544d5..a3c8a2a867 100644 --- a/packages/core/content-releases/admin/src/translations/en.json +++ b/packages/core/content-releases/admin/src/translations/en.json @@ -45,5 +45,6 @@ "dialog.confirmation-message": "Are you sure you want to delete this release?", "page.Details.button.openContentManager": "Open the Content Manager", "pages.Releases.notification.error.title": "Your request could not be processed.", - "pages.Releases.notification.error.message": "Please try again or open another release." + "pages.Releases.notification.error.message": "Please try again or open another release.", + "pages.ReleaseDetails.groupBy.label": "Group by {groupBy}" } diff --git a/packages/core/content-releases/package.json b/packages/core/content-releases/package.json index e15563e03e..f148afbaa4 100644 --- a/packages/core/content-releases/package.json +++ b/packages/core/content-releases/package.json @@ -63,6 +63,7 @@ "@strapi/utils": "4.16.2", "axios": "1.6.0", "formik": "2.4.0", + "lodash": "4.17.21", "react-intl": "6.4.1", "react-redux": "8.1.1", "yup": "0.32.9" diff --git a/packages/core/content-releases/server/src/content-types/release-action/schema.ts b/packages/core/content-releases/server/src/content-types/release-action/schema.ts index 1c400b7239..b6ed375817 100644 --- a/packages/core/content-releases/server/src/content-types/release-action/schema.ts +++ b/packages/core/content-releases/server/src/content-types/release-action/schema.ts @@ -33,6 +33,9 @@ export default { type: 'string', required: true, }, + locale: { + type: 'string', + }, release: { type: 'relation', relation: 'manyToOne', diff --git a/packages/core/content-releases/server/src/controllers/__tests__/release-action.test.ts b/packages/core/content-releases/server/src/controllers/__tests__/release-action.test.ts index 662bb41e42..b4f8be0d5a 100644 --- a/packages/core/content-releases/server/src/controllers/__tests__/release-action.test.ts +++ b/packages/core/content-releases/server/src/controllers/__tests__/release-action.test.ts @@ -112,98 +112,4 @@ describe('Release Action controller', () => { ); }); }); - - describe('findMany', () => { - it('should return the data for an entry', async () => { - mockFindActions.mockResolvedValueOnce({ - results: [ - { - id: 1, - contentType: 'api::contentTypeA.contentTypeA', - entry: { id: 1, name: 'test 1', locale: 'en' }, - }, - { - id: 2, - contentType: 'api::contentTypeB.contentTypeB', - entry: { id: 2, name: 'test 2', locale: 'fr' }, - }, - ], - pagination: {}, - }); - global.strapi = { - plugins: { - // @ts-expect-error Ignore missing properties - i18n: { - services: { - locales: { - find: jest.fn().mockReturnValue([ - { - id: 1, - name: 'English (en)', - code: 'en', - }, - { - id: 2, - name: 'French (fr)', - code: 'fr', - }, - ]), - }, - }, - }, - }, - // @ts-expect-error Ignore missing properties - admin: { - services: { - permission: { - createPermissionsManager: jest.fn(() => ({ - ability: { - can: jest.fn(), - }, - validateQuery: jest.fn(), - sanitizeQuery: jest.fn(() => ctx.query), - })), - }, - }, - }, - }; - - const ctx = { - state: { - userAbility: {}, - }, - params: { - releaseId: 1, - }, - query: {}, - }; - // @ts-expect-error Ignore missing properties - await releaseActionController.findMany(ctx); - - // @ts-expect-error Ignore missing properties - expect(ctx.body.data[0].entry).toEqual({ - id: 1, - contentType: { - displayName: 'contentTypeA', - mainFieldValue: 'test 1', - }, - locale: { - code: 'en', - name: 'English (en)', - }, - }); - // @ts-expect-error Ignore missing properties - expect(ctx.body.data[1].entry).toEqual({ - id: 2, - contentType: { - displayName: 'contentTypeB', - mainFieldValue: 'test 2', - }, - locale: { - code: 'fr', - name: 'French (fr)', - }, - }); - }); - }); }); diff --git a/packages/core/content-releases/server/src/controllers/release-action.ts b/packages/core/content-releases/server/src/controllers/release-action.ts index 7258a58d2c..6338fcd54c 100644 --- a/packages/core/content-releases/server/src/controllers/release-action.ts +++ b/packages/core/content-releases/server/src/controllers/release-action.ts @@ -1,5 +1,4 @@ import type Koa from 'koa'; -import { Entity } from '../../../shared/types'; import { validateReleaseAction, @@ -8,22 +7,12 @@ import { import type { CreateReleaseAction, GetReleaseActions, - ReleaseAction, UpdateReleaseAction, DeleteReleaseAction, } from '../../../shared/contracts/release-actions'; import { getService } from '../utils'; import { RELEASE_ACTION_MODEL_UID } from '../constants'; -interface Locale extends Entity { - name: string; - code: string; -} - -type LocaleDictionary = { - [key: Locale['code']]: Pick; -}; - const releaseActionController = { async create(ctx: Koa.Context) { const releaseId: CreateReleaseAction.Request['params']['releaseId'] = ctx.params.releaseId; @@ -38,6 +27,7 @@ const releaseActionController = { data: releaseAction, }; }, + async findMany(ctx: Koa.Context) { const releaseId: GetReleaseActions.Request['params']['releaseId'] = ctx.params.releaseId; const permissionsManager = strapi.admin.services.permission.createPermissionsManager({ @@ -47,36 +37,14 @@ const releaseActionController = { const query = await permissionsManager.sanitizeQuery(ctx.query); const releaseService = getService('release', { strapi }); - const { results, pagination } = await releaseService.findActions(releaseId, query); - const allReleaseContentTypesDictionary = await releaseService.getContentTypesDataForActions( - releaseId - ); - - const allLocales: Locale[] = await strapi.plugin('i18n').service('locales').find(); - const allLocalesDictionary = allLocales.reduce((acc, locale) => { - acc[locale.code] = { name: locale.name, code: locale.code }; - - return acc; - }, {}); - - const data = results.map((action: ReleaseAction) => { - const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType]; - - return { - ...action, - entry: { - id: action.entry.id, - contentType: { - displayName, - mainFieldValue: action.entry[mainField], - }, - locale: allLocalesDictionary[action.entry.locale], - }, - }; + const { results, pagination } = await releaseService.findActions(releaseId, { + sort: query.groupBy === 'action' ? 'type' : query.groupBy, + ...query, }); + const groupedData = await releaseService.groupActions(results, query.groupBy); ctx.body = { - data, + data: groupedData, meta: { pagination, }, diff --git a/packages/core/content-releases/server/src/services/__tests__/release.test.ts b/packages/core/content-releases/server/src/services/__tests__/release.test.ts index 2681d11ad6..cbb43d275d 100644 --- a/packages/core/content-releases/server/src/services/__tests__/release.test.ts +++ b/packages/core/content-releases/server/src/services/__tests__/release.test.ts @@ -205,4 +205,92 @@ describe('release service', () => { expect(() => releaseService.delete(1)).rejects.toThrow('Release already published'); }); }); + + describe('groupActions', () => { + it('should return the data grouped by contentType', async () => { + const strapiMock = { + ...baseStrapiMock, + plugin: jest.fn().mockReturnValue({ + service: jest.fn().mockReturnValue({ + find: jest.fn().mockReturnValue([ + { name: 'English (en)', code: 'en' }, + { name: 'French (fr)', code: 'fr' }, + ]), + }), + }), + }; + + const mockActions = [ + { + id: 1, + contentType: 'api::contentTypeA.contentTypeA', + locale: 'en', + entry: { id: 1, name: 'test 1' }, + }, + { + id: 2, + contentType: 'api::contentTypeB.contentTypeB', + locale: 'fr', + entry: { id: 2, name: 'test 2' }, + }, + ]; + + // @ts-expect-error Ignore missing properties + const releaseService = createReleaseService({ strapi: strapiMock }); + + // Mock getContentTypesDataForActions inside the release service + releaseService.getContentTypesDataForActions = jest.fn().mockReturnValue({ + 'api::contentTypeA.contentTypeA': { + mainField: 'name', + displayName: 'contentTypeA', + }, + 'api::contentTypeB.contentTypeB': { + mainField: 'name', + displayName: 'contentTypeB', + }, + }); + + // @ts-expect-error ignore missing properties + const groupedData = await releaseService.groupActions(mockActions, 'contentType'); + + expect(groupedData).toEqual({ + contentTypeA: [ + { + id: 1, + locale: 'en', + contentType: 'api::contentTypeA.contentTypeA', + entry: { + id: 1, + contentType: { + displayName: 'contentTypeA', + mainFieldValue: 'test 1', + }, + locale: { + code: 'en', + name: 'English (en)', + }, + }, + }, + ], + contentTypeB: [ + { + id: 2, + locale: 'fr', + contentType: 'api::contentTypeB.contentTypeB', + entry: { + id: 2, + contentType: { + displayName: 'contentTypeB', + mainFieldValue: 'test 2', + }, + locale: { + code: 'fr', + name: 'French (fr)', + }, + }, + }, + ], + }); + }); + }); }); diff --git a/packages/core/content-releases/server/src/services/release.ts b/packages/core/content-releases/server/src/services/release.ts index 9a2f88ba85..b2ebae79a3 100644 --- a/packages/core/content-releases/server/src/services/release.ts +++ b/packages/core/content-releases/server/src/services/release.ts @@ -1,5 +1,8 @@ import { setCreatorFields, errors } from '@strapi/utils'; + import type { LoadedStrapi, EntityService, UID } from '@strapi/types'; + +import _ from 'lodash/fp'; import { RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constants'; import type { GetReleases, @@ -17,10 +20,33 @@ import type { ReleaseAction, UpdateReleaseAction, DeleteReleaseAction, + ReleaseActionGroupBy, } from '../../../shared/contracts/release-actions'; -import type { UserInfo } from '../../../shared/types'; +import type { Entity, UserInfo } from '../../../shared/types'; import { getService } from '../utils'; +interface Locale extends Entity { + name: string; + code: string; +} + +type LocaleDictionary = { + [key: Locale['code']]: Pick; +}; + +const getGroupName = (queryValue?: ReleaseActionGroupBy) => { + switch (queryValue) { + case 'contentType': + return 'entry.contentType.displayName'; + case 'action': + return 'type'; + case 'locale': + return _.getOr('No locale', 'entry.locale.name'); + default: + return 'entry.contentType.displayName'; + } +}; + const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ async create(releaseData: CreateRelease.Request['body'], { user }: { user: UserInfo }) { const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData); @@ -116,7 +142,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ return { ...release, - action: actionForEntry + action: actionForEntry, }; } @@ -166,6 +192,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ data: { type, contentType: entry.contentType, + locale: entry.locale, entry: { id: entry.id, __type: entry.contentType, @@ -181,9 +208,11 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ releaseId: GetReleaseActions.Request['params']['releaseId'], query?: GetReleaseActions.Request['query'] ) { - const result = await strapi.entityService.findOne(RELEASE_MODEL_UID, releaseId); + const release = await strapi.entityService.findOne(RELEASE_MODEL_UID, releaseId, { + fields: ['id'], + }); - if (!result) { + if (!release) { throw new errors.NotFoundError(`No release found for id ${releaseId}`); } @@ -202,34 +231,51 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ return strapi.entityService.count(RELEASE_ACTION_MODEL_UID, query); }, - async getAllContentTypeUids(releaseId: Release['id']) { - const contentTypesFromReleaseActions: { contentType: UID.ContentType }[] = await strapi.db - .queryBuilder(RELEASE_ACTION_MODEL_UID) - .select('content_type') - .where({ - $and: [ - { - release: releaseId, - }, - ], - }) - .groupBy('content_type') - .execute(); + async groupActions(actions: ReleaseAction[], groupBy: ReleaseActionGroupBy) { + const contentTypeUids = actions.reduce((acc, action) => { + if (!acc.includes(action.contentType)) { + acc.push(action.contentType); + } - return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid); + return acc; + }, []); + const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions( + contentTypeUids + ); + const allLocales: Locale[] = await strapi.plugin('i18n').service('locales').find(); + const allLocalesDictionary = allLocales.reduce((acc, locale) => { + acc[locale.code] = { name: locale.name, code: locale.code }; + + return acc; + }, {}); + + const formattedData = actions.map((action: ReleaseAction) => { + const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType]; + + return { + ...action, + entry: { + id: action.entry.id, + contentType: { + displayName, + mainFieldValue: action.entry[mainField], + }, + locale: action.locale ? allLocalesDictionary[action.locale] : null, + }, + }; + }); + + const groupName = getGroupName(groupBy); + return _.groupBy(groupName)(formattedData); }, - async getContentTypesDataForActions(releaseId: Release['id']) { - const contentTypesUids = await this.getAllContentTypeUids(releaseId); - + async getContentTypesDataForActions(contentTypesUids: ReleaseAction['contentType'][]) { const contentManagerContentTypeService = strapi .plugin('content-manager') .service('content-types'); - const contentTypesData: Record< - UID.ContentType, - { mainField: string; displayName: string } - > = {}; + const contentTypesData: Record = + {}; for (const contentTypeUid of contentTypesUids) { const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({ uid: contentTypeUid, diff --git a/packages/core/content-releases/shared/contracts/release-actions.ts b/packages/core/content-releases/shared/contracts/release-actions.ts index 805bdd3cc4..b5c8fa188d 100644 --- a/packages/core/content-releases/shared/contracts/release-actions.ts +++ b/packages/core/content-releases/shared/contracts/release-actions.ts @@ -8,7 +8,7 @@ type ReleaseActionEntry = Entity & { // Entity attributes [key: string]: Attribute.Any; } & { - locale: string; + locale?: string; }; type ReleaseActionEntryData = { @@ -27,6 +27,7 @@ export interface ReleaseAction extends Entity { type: 'publish' | 'unpublish'; entry: ReleaseActionEntry; contentType: Common.UID.ContentType; + locale?: string; release: Release; } @@ -42,6 +43,7 @@ export declare namespace CreateReleaseAction { type: ReleaseAction['type']; entry: { id: ReleaseActionEntry['id']; + locale?: ReleaseActionEntry['locale']; contentType: Common.UID.ContentType; }; }; @@ -56,16 +58,22 @@ export declare namespace CreateReleaseAction { /** * GET /content-releases/:id/actions - Get all release actions */ + +export type ReleaseActionGroupBy = 'contentType' | 'action' | 'locale'; export declare namespace GetReleaseActions { export interface Request { params: { releaseId: Release['id']; }; - query?: Partial>; + query?: Partial> & { + groupBy?: ReleaseActionGroupBy; + }; } export interface Response { - data: Array; + data: { + [key: string]: Array; + }; meta: { pagination: Pagination; }; diff --git a/yarn.lock b/yarn.lock index 98a0c9efae..d86b26726f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8779,6 +8779,7 @@ __metadata: axios: "npm:1.6.0" formik: "npm:2.4.0" koa: "npm:2.13.4" + lodash: "npm:4.17.21" msw: "npm:1.3.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" From b025dffff55e7ced2db572a48f86da3fe6d96da7 Mon Sep 17 00:00:00 2001 From: Bassel Kanso Date: Fri, 5 Jan 2024 12:11:47 +0200 Subject: [PATCH 4/9] Revert "chore(deps): bump graphql-upload to 15.0.2" --- packages/plugins/graphql/package.json | 4 +- .../plugins/graphql/server/src/bootstrap.ts | 3 +- .../src/services/internals/scalars/index.ts | 3 +- yarn.lock | 83 ++++++++----------- 4 files changed, 37 insertions(+), 56 deletions(-) diff --git a/packages/plugins/graphql/package.json b/packages/plugins/graphql/package.json index 80007ea9d7..232417199b 100644 --- a/packages/plugins/graphql/package.json +++ b/packages/plugins/graphql/package.json @@ -61,7 +61,7 @@ "graphql-depth-limit": "^1.1.0", "graphql-playground-middleware-koa": "^1.6.21", "graphql-scalars": "1.22.2", - "graphql-upload": "15.0.2", + "graphql-upload": "^13.0.0", "koa-compose": "^4.1.0", "lodash": "4.17.21", "nexus": "1.3.0", @@ -71,7 +71,7 @@ "@strapi/strapi": "4.16.2", "@strapi/types": "4.16.2", "@types/graphql-depth-limit": "1.1.5", - "@types/graphql-upload": "15.0.2", + "@types/graphql-upload": "8.0.12", "cross-env": "^7.0.3", "eslint-config-custom": "4.16.2", "koa": "2.13.4", diff --git a/packages/plugins/graphql/server/src/bootstrap.ts b/packages/plugins/graphql/server/src/bootstrap.ts index 8e08080f27..92a19adae2 100644 --- a/packages/plugins/graphql/server/src/bootstrap.ts +++ b/packages/plugins/graphql/server/src/bootstrap.ts @@ -5,8 +5,7 @@ import { ApolloServerPluginLandingPageGraphQLPlayground, } from 'apollo-server-core'; import depthLimit from 'graphql-depth-limit'; -// eslint-disable-next-line import/extensions -import graphqlUploadKoa from 'graphql-upload/graphqlUploadKoa.js'; +import { graphqlUploadKoa } from 'graphql-upload'; import type { Config } from 'apollo-server-core'; import type { Strapi } from '@strapi/types'; diff --git a/packages/plugins/graphql/server/src/services/internals/scalars/index.ts b/packages/plugins/graphql/server/src/services/internals/scalars/index.ts index b1bf710e79..baf68663d9 100644 --- a/packages/plugins/graphql/server/src/services/internals/scalars/index.ts +++ b/packages/plugins/graphql/server/src/services/internals/scalars/index.ts @@ -1,6 +1,5 @@ import { GraphQLDateTime, GraphQLLong, GraphQLJSON } from 'graphql-scalars'; -// eslint-disable-next-line import/extensions -import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; +import { GraphQLUpload } from 'graphql-upload'; import { asNexusMethod } from 'nexus'; import TimeScalar from './time'; diff --git a/yarn.lock b/yarn.lock index d86b26726f..81704b9696 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9287,7 +9287,7 @@ __metadata: "@strapi/types": "npm:4.16.2" "@strapi/utils": "npm:4.16.2" "@types/graphql-depth-limit": "npm:1.1.5" - "@types/graphql-upload": "npm:15.0.2" + "@types/graphql-upload": "npm:8.0.12" apollo-server-core: "npm:3.12.1" apollo-server-koa: "npm:3.10.0" cross-env: "npm:^7.0.3" @@ -9296,7 +9296,7 @@ __metadata: graphql-depth-limit: "npm:^1.1.0" graphql-playground-middleware-koa: "npm:^1.6.21" graphql-scalars: "npm:1.22.2" - graphql-upload: "npm:15.0.2" + graphql-upload: "npm:^13.0.0" koa: "npm:2.13.4" koa-compose: "npm:^4.1.0" lodash: "npm:4.17.21" @@ -10164,15 +10164,6 @@ __metadata: languageName: node linkType: hard -"@types/busboy@npm:^1.5.0": - version: 1.5.3 - resolution: "@types/busboy@npm:1.5.3" - dependencies: - "@types/node": "npm:*" - checksum: 9ec0a125723e594816d06f6ddf5a4f8dda4855734719bb6e38bdc6fdaf59416f270744862ee54c755075af26e9f5467cc00db803ae1d88f45c1431f61a48ae58 - languageName: node - linkType: hard - "@types/cacheable-request@npm:^6.0.1": version: 6.0.2 resolution: "@types/cacheable-request@npm:6.0.2" @@ -10458,15 +10449,15 @@ __metadata: languageName: node linkType: hard -"@types/graphql-upload@npm:15.0.2": - version: 15.0.2 - resolution: "@types/graphql-upload@npm:15.0.2" +"@types/graphql-upload@npm:8.0.12": + version: 8.0.12 + resolution: "@types/graphql-upload@npm:8.0.12" dependencies: "@types/express": "npm:*" "@types/koa": "npm:*" fs-capacitor: "npm:^8.0.0" graphql: "npm:0.13.1 - 16" - checksum: 3e88a03082ce1e1de39b414a096fbd47cf1f97b6027c93cc71a216229bdfb248f54f1afc209e970e1ae3856153c20cf40d458febe348b287be237f62ea84ffc0 + checksum: 394d5e05f4d5a205dc53f66670af28de73bc73b0910e68907e6d01348caeeb27478621cfdb238a71668f0cca7347ed362c2f1a68266da4b823707dc4daba92ee languageName: node linkType: hard @@ -10927,13 +10918,6 @@ __metadata: languageName: node linkType: hard -"@types/object-path@npm:^0.11.1": - version: 0.11.4 - resolution: "@types/object-path@npm:0.11.4" - checksum: 7f1f5cb18b651d21e7861da176d8f87526c936ed949a8126a2692195cbe65734ed1a1a22c06a24a54afe1890483a3d6b074b402ebfca7a7567c1c287b588f563 - languageName: node - linkType: hard - "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -13765,12 +13749,12 @@ __metadata: languageName: node linkType: hard -"busboy@npm:^1.6.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" +"busboy@npm:^0.3.1": + version: 0.3.1 + resolution: "busboy@npm:0.3.1" dependencies: - streamsearch: "npm:^1.1.0" - checksum: bee10fa10ea58e7e3e7489ffe4bda6eacd540a17de9f9cd21cc37e297b2dd9fe52b2715a5841afaec82900750d810d01d7edb4b2d456427f449b92b417579763 + dicer: "npm:0.3.0" + checksum: a5ac7fcd7c7abb65051f2bca834c0336ef6e046af4f3e1c7e730436fb5ec00d6b2bd4283faac2eb527f054793af823fe8e08a0d2c857a59b0702f1a29f89fc58 languageName: node linkType: hard @@ -15941,6 +15925,15 @@ __metadata: languageName: node linkType: hard +"dicer@npm:0.3.0": + version: 0.3.0 + resolution: "dicer@npm:0.3.0" + dependencies: + streamsearch: "npm:0.1.2" + checksum: 1e92ab2f88b20483caef916293e98f3262a28f281a42a2d9e4691319abec3e6b06ff0c7ee962e1b4a54edea742442a726cc02ac0aad98f89f694d18914c176eb + languageName: node + linkType: hard + "diff-sequences@npm:^29.2.0": version: 29.2.0 resolution: "diff-sequences@npm:29.2.0" @@ -19547,27 +19540,17 @@ __metadata: languageName: node linkType: hard -"graphql-upload@npm:15.0.2": - version: 15.0.2 - resolution: "graphql-upload@npm:15.0.2" +"graphql-upload@npm:^13.0.0": + version: 13.0.0 + resolution: "graphql-upload@npm:13.0.0" dependencies: - "@types/busboy": "npm:^1.5.0" - "@types/node": "npm:*" - "@types/object-path": "npm:^0.11.1" - busboy: "npm:^1.6.0" + busboy: "npm:^0.3.1" fs-capacitor: "npm:^6.2.0" - http-errors: "npm:^2.0.0" + http-errors: "npm:^1.8.1" object-path: "npm:^0.11.8" peerDependencies: - "@types/express": ^4.0.29 - "@types/koa": ^2.11.4 - graphql: ^16.3.0 - peerDependenciesMeta: - "@types/express": - optional: true - "@types/koa": - optional: true - checksum: bf0a7f92842882ed982a625eaf0d8c2fa36f32096d75c80b89989f17cb10e8193a033f69192d457555974f187e2e8f4eb4bf3df94a86b86c19493f9bcc7849ef + graphql: 0.13.1 - 16 + checksum: 35c4c577fa25ad25aaf72655b4ebc45660489bee700e3548ca3097323f6f58c8cbcaa2f7603d215038fdcd99068623ffd5ef1f9e1280bd8cd9e99e7c9f1b979b languageName: node linkType: hard @@ -20041,7 +20024,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:1.8.1, http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:^1.8.0, http-errors@npm:~1.8.0": +"http-errors@npm:1.8.1, http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:^1.8.0, http-errors@npm:^1.8.1, http-errors@npm:~1.8.0": version: 1.8.1 resolution: "http-errors@npm:1.8.1" dependencies: @@ -20054,7 +20037,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -30460,10 +30443,10 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 612c2b2a7dbcc859f74597112f80a42cbe4d448d03da790d5b7b39673c1197dd3789e91cd67210353e58857395d32c1e955a9041c4e6d5bae723436b3ed9ed14 +"streamsearch@npm:0.1.2": + version: 0.1.2 + resolution: "streamsearch@npm:0.1.2" + checksum: 2c9407ee6682f100a9026b4b712d01ce3889fc818b928746eeb92fb4c0cf4ee79b74af27893fd766e4a36bbed08969a8e0bd0d0be5d30b2c9028859071f8f02b languageName: node linkType: hard From cf88e1ba2e1dbd3885a2a99c5689225ce8a579ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Ch=C3=A1vez?= Date: Tue, 9 Jan 2024 08:58:32 +0100 Subject: [PATCH 5/9] enhancement(content-releases): add new status column to Release Details (#19154) * enhancement(content-releases): add new status column to Release Details page * fix lint & ts errors --- .../admin/src/pages/ReleaseDetailsPage.tsx | 70 ++++++++++++++++++- .../pages/tests/ReleaseDetailsPage.test.tsx | 40 ++++++++++- .../pages/tests/mockReleaseDetailsPageData.ts | 7 +- .../admin/src/translations/en.json | 7 +- .../src/services/__tests__/release.test.ts | 6 +- .../server/src/services/release.ts | 1 + .../shared/contracts/release-actions.ts | 2 + 7 files changed, 125 insertions(+), 8 deletions(-) diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 9b766b7c20..06ff156b75 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -15,6 +15,7 @@ import { Badge, SingleSelect, SingleSelectOption, + Icon, } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; import { @@ -31,7 +32,7 @@ import { ConfirmDialog, useRBAC, } from '@strapi/helper-plugin'; -import { ArrowLeft, More, Pencil, Trash } from '@strapi/icons'; +import { ArrowLeft, CheckCircle, More, Pencil, Trash } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { useParams, useHistory, Link as ReactRouterLink, Redirect } from 'react-router-dom'; import styled from 'styled-components'; @@ -119,6 +120,58 @@ const PopoverButton = ({ onClick, disabled, children }: PopoverButtonProps) => { ); }; +interface EntryValidationTextProps { + status: ReleaseAction['entry']['status']; + action: ReleaseAction['type']; +} + +const EntryValidationText = ({ status, action }: EntryValidationTextProps) => { + const { formatMessage } = useIntl(); + + if (action == 'publish') { + return ( + + + {status === 'published' ? ( + + {formatMessage({ + id: 'content-releases.pages.ReleaseDetails.entry-validation.already-published', + defaultMessage: 'Already published', + })} + + ) : ( + + {formatMessage({ + id: 'content-releases.pages.ReleaseDetails.entry-validation.ready-to-publish', + defaultMessage: 'Ready to publish', + })} + + )} + + ); + } + + return ( + + + {status === 'draft' ? ( + + {formatMessage({ + id: 'content-releases.pages.ReleaseDetails.entry-validation.already-unpublished', + defaultMessage: 'Already unpublished', + })} + + ) : ( + + {formatMessage({ + id: 'content-releases.pages.ReleaseDetails.entry-validation.ready-to-unpublish', + defaultMessage: 'Ready to unpublish', + })} + + )} + + ); +}; interface ReleaseDetailsLayoutProps { toggleEditReleaseModal: () => void; toggleWarningSubmit: () => void; @@ -546,6 +599,16 @@ const ReleaseDetailsBody = () => { })} name="action" /> + {!release.releasedAt && ( + + )} @@ -589,6 +652,11 @@ const ReleaseDetailsBody = () => { /> )} + {!release.releasedAt && ( + + + + )} 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 086ce280e3..a0aee1a6f7 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,4 +1,5 @@ import { useRBAC } from '@strapi/helper-plugin'; +import { within } from '@testing-library/react'; import { render, server, screen } from '@tests/utils'; import { rest } from 'msw'; @@ -147,8 +148,7 @@ describe('Releases details page', () => { expect(publishButton).not.toBeInTheDocument(); expect(screen.queryByRole('radio', { name: 'publish' })).not.toBeInTheDocument(); - const container = screen.getByText(/This entry was/); - expect(container.querySelector('span')).toHaveTextContent('published'); + expect(screen.getByRole('gridcell', { name: /This entry was published/i })).toBeInTheDocument(); }); it('renders the details page with the delete and edit buttons disabled', async () => { @@ -215,4 +215,40 @@ describe('Releases details page', () => { expect(tables).toHaveLength(2); }); + + it('show the right status based on the action and status', async () => { + server.use( + rest.get('/content-releases/:releaseId', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withActionsHeaderData)) + ) + ); + + server.use( + rest.get('/content-releases/:releaseId/actions', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withMultipleActionsBodyData)) + ) + ); + + render(, { + initialEntries: [{ pathname: `/content-releases/1` }], + }); + + const releaseTitle = await screen.findByText( + mockReleaseDetailsPageData.withActionsHeaderData.data.name + ); + expect(releaseTitle).toBeInTheDocument(); + + const cat1Row = screen.getByRole('row', { name: /cat1/i }); + expect(within(cat1Row).getByRole('gridcell', { name: 'Ready to publish' })).toBeInTheDocument(); + + const cat2Row = screen.getByRole('row', { name: /cat2/i }); + expect( + within(cat2Row).getByRole('gridcell', { name: 'Ready to unpublish' }) + ).toBeInTheDocument(); + + const add1Row = screen.getByRole('row', { name: /add1/i }); + expect( + within(add1Row).getByRole('gridcell', { name: 'Already published' }) + ).toBeInTheDocument(); + }); }); diff --git a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts index 122af5159b..8ee68ee9d0 100644 --- a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts +++ b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts @@ -147,11 +147,12 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = { name: 'English (en)', code: 'en', }, + status: 'draft', }, }, { id: 4, - type: 'publish', + type: 'unpublish', contentType: 'api::category.category', createdAt: '2023-12-05T09:03:57.155Z', updatedAt: '2023-12-05T09:03:57.155Z', @@ -159,12 +160,13 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = { id: 2, contentType: { displayName: 'Category', - mainFieldValue: 'cat1', + mainFieldValue: 'cat2', }, locale: { name: 'English (en)', code: 'en', }, + status: 'published', }, }, ], @@ -185,6 +187,7 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = { name: 'English (en)', code: 'en', }, + status: 'published', }, }, ], diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json index a3c8a2a867..c6330e553d 100644 --- a/packages/core/content-releases/admin/src/translations/en.json +++ b/packages/core/content-releases/admin/src/translations/en.json @@ -40,11 +40,16 @@ "page.ReleaseDetails.table.header.label.locale": "locale", "page.ReleaseDetails.table.header.label.content-type": "content-type", "page.ReleaseDetails.table.header.label.action": "action", + "content-releases.page.ReleaseDetails.table.header.label.status": "status", "page.ReleaseDetails.table.action-published": "This entry was {isPublish, select, true {published} other {unpublished}}.", "pages.ReleaseDetails.publish-notification-success": "Release was published successfully.", "dialog.confirmation-message": "Are you sure you want to delete this release?", "page.Details.button.openContentManager": "Open the Content Manager", "pages.Releases.notification.error.title": "Your request could not be processed.", "pages.Releases.notification.error.message": "Please try again or open another release.", - "pages.ReleaseDetails.groupBy.label": "Group by {groupBy}" + "pages.ReleaseDetails.groupBy.label": "Group by {groupBy}", + "content-releases.pages.ReleaseDetails.entry-validation.already-published": "Already published", + "content-releases.pages.ReleaseDetails.entry-validation.ready-to-publish": "Ready to publish", + "content-releases.pages.ReleaseDetails.entry-validation.already-unpublished": "Already unpublished", + "content-releases.pages.ReleaseDetails.entry-validation.ready-to-unpublish": "Ready to unpublish" } diff --git a/packages/core/content-releases/server/src/services/__tests__/release.test.ts b/packages/core/content-releases/server/src/services/__tests__/release.test.ts index cbb43d275d..dd06b75427 100644 --- a/packages/core/content-releases/server/src/services/__tests__/release.test.ts +++ b/packages/core/content-releases/server/src/services/__tests__/release.test.ts @@ -225,13 +225,13 @@ describe('release service', () => { id: 1, contentType: 'api::contentTypeA.contentTypeA', locale: 'en', - entry: { id: 1, name: 'test 1' }, + entry: { id: 1, name: 'test 1', publishedAt: '2021-01-01' }, }, { id: 2, contentType: 'api::contentTypeB.contentTypeB', locale: 'fr', - entry: { id: 2, name: 'test 2' }, + entry: { id: 2, name: 'test 2', publishedAt: null }, }, ]; @@ -269,6 +269,7 @@ describe('release service', () => { code: 'en', name: 'English (en)', }, + status: 'published', }, }, ], @@ -287,6 +288,7 @@ describe('release service', () => { code: 'fr', name: 'French (fr)', }, + status: 'draft', }, }, ], diff --git a/packages/core/content-releases/server/src/services/release.ts b/packages/core/content-releases/server/src/services/release.ts index b2ebae79a3..6c8c689782 100644 --- a/packages/core/content-releases/server/src/services/release.ts +++ b/packages/core/content-releases/server/src/services/release.ts @@ -261,6 +261,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ mainFieldValue: action.entry[mainField], }, locale: action.locale ? allLocalesDictionary[action.locale] : null, + status: action.entry.publishedAt ? 'published' : 'draft', }, }; }); diff --git a/packages/core/content-releases/shared/contracts/release-actions.ts b/packages/core/content-releases/shared/contracts/release-actions.ts index b5c8fa188d..174a793c2b 100644 --- a/packages/core/content-releases/shared/contracts/release-actions.ts +++ b/packages/core/content-releases/shared/contracts/release-actions.ts @@ -9,6 +9,7 @@ type ReleaseActionEntry = Entity & { [key: string]: Attribute.Any; } & { locale?: string; + status: 'published' | 'draft'; }; type ReleaseActionEntryData = { @@ -21,6 +22,7 @@ type ReleaseActionEntryData = { mainFieldValue?: string; displayName: string; }; + status: 'published' | 'draft'; }; export interface ReleaseAction extends Entity { From be8c5a115dfdc155c370c94fd39eb28951d0935e Mon Sep 17 00:00:00 2001 From: markkaylor Date: Tue, 9 Jan 2024 10:07:14 +0100 Subject: [PATCH 6/9] fix(content-releases): update options to match designs (#19171) --- .../admin/src/pages/ReleaseDetailsPage.tsx | 49 +++++++++++-------- .../admin/src/translations/en.json | 11 +++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 06ff156b75..60cf572807 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -380,24 +380,31 @@ export const ReleaseDetailsLayout = ({ ); }; -const GROUP_BY_OPTIONS = [ - { - label: 'Content Type', - value: 'contentType', - }, - { - label: 'Locale', - value: 'locale', - }, - { - label: 'Action', - value: 'action', - }, -]; - /* ------------------------------------------------------------------------------------------------- * ReleaseDetailsBody * -----------------------------------------------------------------------------------------------*/ +const GROUP_BY_OPTIONS = ['contentType', 'locale', 'action'] as const; +const getGroupByOptionLabel = (value: (typeof GROUP_BY_OPTIONS)[number]) => { + if (value === 'locale') { + return { + id: 'content-releases.pages.ReleaseDetails.groupBy.option.locales', + defaultMessage: 'Locales', + }; + } + + if (value === 'action') { + return { + id: 'content-releases.pages.ReleaseDetails.groupBy.option.actions', + defaultMessage: 'Actions', + }; + } + + return { + id: 'content-releases.pages.ReleaseDetails.groupBy.option.content-type', + defaultMessage: 'Content-Types', + }; +}; + const ReleaseDetailsBody = () => { const { formatMessage } = useIntl(); const { releaseId } = useParams<{ releaseId: string }>(); @@ -410,7 +417,9 @@ const ReleaseDetailsBody = () => { isError: isReleaseError, error: releaseError, } = useGetReleaseQuery({ id: releaseId }); + const release = releaseData?.data; + const selectedGroupBy = query?.groupBy || 'contentType'; const { isLoading, @@ -527,13 +536,13 @@ const ReleaseDetailsBody = () => { formatMessage( { - id: `pages.ReleaseDetails.groupBy.label}`, + id: `content-releases.pages.ReleaseDetails.groupBy.label`, defaultMessage: `Group by {groupBy}`, }, { @@ -541,12 +550,12 @@ const ReleaseDetailsBody = () => { } ) } - value={query?.groupBy || 'contentType'} + value={formatMessage(getGroupByOptionLabel(selectedGroupBy))} onChange={(value) => setQuery({ groupBy: value as ReleaseActionGroupBy })} > {GROUP_BY_OPTIONS.map((option) => ( - - {option.label} + + {formatMessage(getGroupByOptionLabel(option))} ))} diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json index c6330e553d..73eeed623e 100644 --- a/packages/core/content-releases/admin/src/translations/en.json +++ b/packages/core/content-releases/admin/src/translations/en.json @@ -48,8 +48,11 @@ "pages.Releases.notification.error.title": "Your request could not be processed.", "pages.Releases.notification.error.message": "Please try again or open another release.", "pages.ReleaseDetails.groupBy.label": "Group by {groupBy}", - "content-releases.pages.ReleaseDetails.entry-validation.already-published": "Already published", - "content-releases.pages.ReleaseDetails.entry-validation.ready-to-publish": "Ready to publish", - "content-releases.pages.ReleaseDetails.entry-validation.already-unpublished": "Already unpublished", - "content-releases.pages.ReleaseDetails.entry-validation.ready-to-unpublish": "Ready to unpublish" + "pages.ReleaseDetails.entry-validation.already-published": "Already published", + "pages.ReleaseDetails.entry-validation.ready-to-publish": "Ready to publish", + "pages.ReleaseDetails.entry-validation.already-unpublished": "Already unpublished", + "pages.ReleaseDetails.entry-validation.ready-to-unpublish": "Ready to unpublish", + "pages.ReleaseDetails.groupBy.option.content-type": "Content-Types", + "pages.ReleaseDetails.groupBy.option.locales": "Locales", + "pages.ReleaseDetails.groupBy.option.actions": "Actions" } From a5f9f4b678baec25d9a31555c27a1ea1234a6438 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 9 Jan 2024 11:29:03 +0100 Subject: [PATCH 7/9] fix(content-releases): handle the lastname null in the createdBy content (#19174) --- .../content-releases/admin/src/pages/ReleaseDetailsPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 60cf572807..2cc1bd4f93 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -269,7 +269,9 @@ export const ReleaseDetailsLayout = ({ } const totalEntries = release.actions.meta.count || 0; - const createdBy = `${release.createdBy.firstname} ${release.createdBy.lastname}`; + const createdBy = release.createdBy.lastname + ? `${release.createdBy.firstname} ${release.createdBy.lastname}` + : `${release.createdBy.firstname}`; return (
From 00f3e143088a5bd3f6a0f5991d76ba7349c22534 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 9 Jan 2024 11:30:25 +0100 Subject: [PATCH 8/9] fix(content-releases): change the submit button action name on edit (#19167) * fix(content-releases): change submit label on the release edit modal * fix(content-releases): disable button if the input name is empty * fix(content-releases): use location to identify if the modal is on create or edit * fix(content-releases): change variable name and logic for the button labels based on review comments * fix(content-releases): change translation content for the Modal title and button --- .../admin/src/components/ReleaseModal.tsx | 34 ++++++++++++++----- .../components/tests/ReleaseModal.test.tsx | 34 +++++++++++++------ .../admin/src/translations/en.json | 4 +-- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx index 05596f7626..54ac74edb3 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx @@ -9,8 +9,10 @@ import { } from '@strapi/design-system'; import { Formik, Form } from 'formik'; import { useIntl } from 'react-intl'; +import { useLocation } from 'react-router-dom'; import { RELEASE_SCHEMA } from '../../../shared/validation-schemas'; +import { pluginId } from '../pluginId'; export interface FormValues { name: string; @@ -30,15 +32,21 @@ export const ReleaseModal = ({ isLoading = false, }: ReleaseModalProps) => { const { formatMessage } = useIntl(); + const { pathname } = useLocation(); + const isCreatingRelease = pathname === `/plugins/${pluginId}`; return ( - {formatMessage({ - id: 'content-releases.modal.add-release-title', - defaultMessage: 'New release', - })} + {formatMessage( + { + id: 'content-releases.modal.title', + defaultMessage: + '{isCreatingRelease, select, true {New release} other {Edit release}}', + }, + { isCreatingRelease: isCreatingRelease } + )} } endActions={ - } /> 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 index ca4c74f34e..2dd8636a1c 100644 --- a/packages/core/content-releases/admin/src/components/tests/ReleaseModal.test.tsx +++ b/packages/core/content-releases/admin/src/components/tests/ReleaseModal.test.tsx @@ -1,18 +1,22 @@ import { within } from '@testing-library/react'; import { render, screen } from '@tests/utils'; +import { MemoryRouter } from 'react-router-dom'; +import { pluginId } from '../../pluginId'; 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', { @@ -35,7 +39,7 @@ describe('ReleaseModal', () => { }); it('renders correctly the dialog content on update', async () => { const handleCloseMocked = jest.fn(); - render( + const { user } = render( { 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, + // disable the submit button when there are no changes inside the input + const dialogSaveButton = within(dialogContainer).getByRole('button', { + name: /save/i, }); - expect(dialogContinueButton).toBeEnabled(); + expect(dialogSaveButton).toBeDisabled(); + + // change the input value and enable the submit button + await user.type(inputElement, 'new content'); + expect(dialogSaveButton).toBeEnabled(); + + // change the input to an empty value and disable the submit button + await user.clear(inputElement); + expect(dialogSaveButton).toBeDisabled(); }); }); diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json index 73eeed623e..c34d1174bf 100644 --- a/packages/core/content-releases/admin/src/translations/en.json +++ b/packages/core/content-releases/admin/src/translations/en.json @@ -26,9 +26,9 @@ "header.actions.created.description": " by {createdBy}", "modal.release-created-notification-success": "Release created", "modal.release-updated-notification-success": "Release updated", - "modal.add-release-title": "New Release", + "modal.title": "{isCreatingRelease, select, true {New release} other {Edit release}}", "modal.form.input.label.release-name": "Name", - "modal.form.button.submit": "Continue", + "modal.form.button.submit": "{isCreatingRelease, select, true {Continue} other {Save}}", "pages.Details.header-subtitle": "{number, plural, =0 {No entries} one {# entry} other {# entries}}", "pages.Releases.tab-group.label": "Releases list", "pages.Releases.tab.pending": "Pending", From 0bb09577cab2de535ce223d35909620ff913264d Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 10 Jan 2024 09:38:57 +0100 Subject: [PATCH 9/9] fix(content-releases): move the check permission to the main App component (#19183) --- .../content-releases/admin/src/pages/App.tsx | 20 +++++++++---------- .../admin/src/pages/ReleaseDetailsPage.tsx | 8 +------- .../admin/src/pages/ReleasesPage.tsx | 8 +------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/core/content-releases/admin/src/pages/App.tsx b/packages/core/content-releases/admin/src/pages/App.tsx index a76145d015..9e39d1800e 100644 --- a/packages/core/content-releases/admin/src/pages/App.tsx +++ b/packages/core/content-releases/admin/src/pages/App.tsx @@ -1,19 +1,19 @@ +import { CheckPagePermissions } from '@strapi/helper-plugin'; import { Route, Switch } from 'react-router-dom'; +import { PERMISSIONS } from '../constants'; import { pluginId } from '../pluginId'; -import { ProtectedReleaseDetailsPage } from './ReleaseDetailsPage'; -import { ProtectedReleasesPage } from './ReleasesPage'; +import { ReleaseDetailsPage } from './ReleaseDetailsPage'; +import { ReleasesPage } from './ReleasesPage'; export const App = () => { return ( - - - - + + + + + + ); }; diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 2cc1bd4f93..b5c1bf60d0 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -816,10 +816,4 @@ const ReleaseDetailsPage = () => { ); }; -const ProtectedReleaseDetailsPage = () => ( - - - -); - -export { ReleaseDetailsPage, ProtectedReleaseDetailsPage }; +export { ReleaseDetailsPage }; diff --git a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx index 67ae87d039..f02f2cce9e 100644 --- a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx @@ -349,10 +349,4 @@ const ReleasesPage = () => { ); }; -const ProtectedReleasesPage = () => ( - - - -); - -export { ReleasesPage, ProtectedReleasesPage }; +export { ReleasesPage };