From aa65f33dacc4533cd2701aacefa3ad97dc1343db Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Mon, 8 Jan 2024 13:14:15 +0000 Subject: [PATCH 01/22] fix(plugin-graphql): allow to use GET queries for graphql GET queries are often advantagous for caching, cdns, etc This removes a line that wasnt required, that blocked GET queries from being authenticated on /graphql --- api-tests/plugins/graphql/crud.test.api.js | 42 +++++++++++++++++++ api-tests/plugins/graphql/utils.test.api.js | 25 +++++++++++ .../plugins/graphql/server/src/bootstrap.ts | 3 -- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 api-tests/plugins/graphql/utils.test.api.js diff --git a/api-tests/plugins/graphql/crud.test.api.js b/api-tests/plugins/graphql/crud.test.api.js index 13c85de3c1..483506097d 100644 --- a/api-tests/plugins/graphql/crud.test.api.js +++ b/api-tests/plugins/graphql/crud.test.api.js @@ -137,6 +137,48 @@ describe('Test Graphql API End to End', () => { data.posts = res.body.data.posts.data.map(({ id, attributes }) => ({ id, ...attributes })); }); + test('List posts with GET', async () => { + const graphqlQueryGET = (body) => { + return rq({ + url: '/graphql', + method: 'GET', + qs: body, + }); + }; + + const res = await graphqlQueryGET({ + query: /* GraphQL */ ` + { + posts { + data { + id + attributes { + name + bigint + nullable + category + } + } + } + } + `, + }); + + const { body } = res; + + expect(res.statusCode).toBe(200); + expect(body).toMatchObject({ + data: { + posts: { + data: postsPayload.map((entry) => ({ + id: expect.any(String), + attributes: omit('id', entry), + })), + }, + }, + }); + }); + test('List posts with limit', async () => { const res = await graphqlQuery({ query: /* GraphQL */ ` diff --git a/api-tests/plugins/graphql/utils.test.api.js b/api-tests/plugins/graphql/utils.test.api.js new file mode 100644 index 0000000000..f7e805a9d0 --- /dev/null +++ b/api-tests/plugins/graphql/utils.test.api.js @@ -0,0 +1,25 @@ +'use strict'; + +// Helpers. +const { createStrapiInstance } = require('api-tests/strapi'); +const request = require('supertest'); + +let strapi; + +describe('Test Graphql Utils', () => { + beforeAll(async () => { + strapi = await createStrapiInstance(); + }); + + afterAll(async () => { + await strapi.destroy(); + }); + + test('Load Graphql playground', async () => { + const supertestAgent = request.agent(strapi.server.httpServer); + const res = await supertestAgent.get('/graphql').set('accept', 'text/html'); + + expect(res.statusCode).toBe(200); + expect(res.text).toContain('GraphQL Playground'); + }); +}); diff --git a/packages/plugins/graphql/server/src/bootstrap.ts b/packages/plugins/graphql/server/src/bootstrap.ts index 92a19adae2..dcbccaa5b7 100644 --- a/packages/plugins/graphql/server/src/bootstrap.ts +++ b/packages/plugins/graphql/server/src/bootstrap.ts @@ -115,9 +115,6 @@ export async function bootstrap({ strapi }: { strapi: Strapi }) { }, }; - // allow graphql playground to load without authentication - if (ctx.request.method === 'GET') return next(); - return strapi.auth.authenticate(ctx, next); }, From 5f827ef606d8441cbe5f751bc5a3fbe6ccf3abd4 Mon Sep 17 00:00:00 2001 From: Bassel Kanso Date: Thu, 21 Mar 2024 11:48:30 +0200 Subject: [PATCH 02/22] fix: add support for private upload providers --- .../strapi/providers/local-source/assets.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts b/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts index f15d7e8d05..44582c3b56 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts @@ -61,6 +61,30 @@ function getFileStats(filepath: string, isLocal = false): Promise<{ size: number }); }); } + +async function signUrl(isLocalProvider: boolean, file: any) { + if (isLocalProvider) return; + const { provider } = strapi.plugins.upload; + const { provider: providerName } = strapi.config.get('plugin.upload') as any; + const isPrivate = await provider.isPrivate(); + if (file.provider === providerName && isPrivate) { + const signUrl = async (file: any) => { + const signedUrl = await provider.getSignedUrl(file); + file.url = signedUrl.url; + file.isUrlSigned = true; + }; + + // Sign the original file + await signUrl(file); + // Sign each file format + if (file.formats) { + for (const format of Object.keys(file.formats)) { + await signUrl(file.formats[format]); + } + } + } +} + /** * Generate and consume assets streams in order to stream each file individually */ @@ -76,6 +100,7 @@ export const createAssetsStream = (strapi: LoadedStrapi): Duplex => { for await (const file of stream) { const isLocalProvider = file.provider === 'local'; + await signUrl(isLocalProvider, file); const filepath = isLocalProvider ? join(strapi.dirs.static.public, file.url) : file.url; const stats = await getFileStats(filepath, isLocalProvider); const stream = getFileStream(filepath, isLocalProvider); From 0db935094e132e62737f2a4637c9805edd901ef7 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Thu, 21 Mar 2024 11:06:31 +0100 Subject: [PATCH 03/22] fix(content-releases): fix creation of utc time based when sending to back --- .../content-releases/admin/src/components/ReleaseModal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx index b7456b5f39..27ec368df5 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx @@ -62,9 +62,8 @@ export const ReleaseModal = ({ const getScheduledTimestamp = (values: FormValues) => { const { date, time, timezone } = values; if (!date || !time || !timezone) return null; - const formattedDate = parse(time, 'HH:mm', new Date(date)); const timezoneWithoutOffset = timezone.split('&')[1]; - return zonedTimeToUtc(formattedDate, timezoneWithoutOffset); + return zonedTimeToUtc(`${date} ${time}`, timezoneWithoutOffset); }; /** From e2fea215bb00873bd9bfaabbf2e72c763884ef9a Mon Sep 17 00:00:00 2001 From: Bassel Kanso Date: Thu, 21 Mar 2024 12:31:38 +0200 Subject: [PATCH 04/22] fix: refactor signing url in DTS --- .../src/strapi/providers/local-source/assets.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts b/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts index 44582c3b56..e25280480b 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-source/assets.ts @@ -5,7 +5,7 @@ import { Duplex, PassThrough, Readable } from 'stream'; import { stat, createReadStream, ReadStream } from 'fs-extra'; import type { LoadedStrapi } from '@strapi/types'; -import type { IAsset } from '../../../../types'; +import type { IAsset, IFile } from '../../../../types'; const protocolForPath = (filepath: string) => { return filepath?.startsWith('https') ? https : http; @@ -62,16 +62,14 @@ function getFileStats(filepath: string, isLocal = false): Promise<{ size: number }); } -async function signUrl(isLocalProvider: boolean, file: any) { - if (isLocalProvider) return; +async function signFile(file: IFile) { const { provider } = strapi.plugins.upload; - const { provider: providerName } = strapi.config.get('plugin.upload') as any; + const { provider: providerName } = strapi.config.get('plugin.upload') as { provider: string }; const isPrivate = await provider.isPrivate(); - if (file.provider === providerName && isPrivate) { - const signUrl = async (file: any) => { + if (file?.provider === providerName && isPrivate) { + const signUrl = async (file: IFile) => { const signedUrl = await provider.getSignedUrl(file); file.url = signedUrl.url; - file.isUrlSigned = true; }; // Sign the original file @@ -100,7 +98,9 @@ export const createAssetsStream = (strapi: LoadedStrapi): Duplex => { for await (const file of stream) { const isLocalProvider = file.provider === 'local'; - await signUrl(isLocalProvider, file); + if (!isLocalProvider) { + await signFile(file); + } const filepath = isLocalProvider ? join(strapi.dirs.static.public, file.url) : file.url; const stats = await getFileStats(filepath, isLocalProvider); const stream = getFileStream(filepath, isLocalProvider); From 47305a2c1e3bec251f9b610f228d529857f7eae6 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Thu, 21 Mar 2024 12:36:50 +0100 Subject: [PATCH 05/22] fix editing date --- .../core/content-releases/admin/src/components/ReleaseModal.tsx | 2 +- .../content-releases/admin/src/pages/ReleaseDetailsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx index 27ec368df5..fc203a9a4e 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx @@ -28,7 +28,7 @@ import { getTimezoneOffset } from '../utils/time'; export interface FormValues { name: string; - date: Date | null; + date: string | null; time: string; timezone: string | null; isScheduled?: boolean; diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 7badafc89f..91fbd21bcb 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -878,7 +878,7 @@ const ReleaseDetailsPage = () => { const scheduledAt = releaseData?.scheduledAt && timezone ? utcToZonedTime(releaseData.scheduledAt, timezone) : null; // Just get the date and time to display without considering updated timezone time - const date = scheduledAt ? new Date(format(scheduledAt, 'yyyy-MM-dd')) : null; + const date = scheduledAt ? format(scheduledAt, 'yyyy-MM-dd') : null; const time = scheduledAt ? format(scheduledAt, 'HH:mm') : ''; const handleEditRelease = async (values: FormValues) => { From dfbcb2792b1eec1eff5ee7b99796b9005ef96a0c Mon Sep 17 00:00:00 2001 From: Christian Capeans Date: Thu, 21 Mar 2024 13:39:07 +0100 Subject: [PATCH 06/22] fix: resolve components uid --- .../strategies/restore/entities.ts | 51 +------- .../src/utils/__tests__/components.test.ts | 118 ++++++++++++++++++ .../data-transfer/src/utils/components.ts | 55 +++++++- 3 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 packages/core/data-transfer/src/utils/__tests__/components.test.ts diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts index 2730f88a4f..c44adbdb6a 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts @@ -1,5 +1,5 @@ import { Writable } from 'stream'; -import type { LoadedStrapi, Common, Schema } from '@strapi/types'; +import type { LoadedStrapi, Common, Schema, Attribute } from '@strapi/types'; import { get, last } from 'lodash/fp'; @@ -7,6 +7,7 @@ import { ProviderTransferError } from '../../../../../errors/providers'; import type { IEntity, Transaction } from '../../../../../../types'; import { json } from '../../../../../utils'; import * as queries from '../../../../queries'; +import { resolveComponentUID } from '../../../../../utils/components'; interface IEntitiesRestoreStreamOptions { strapi: LoadedStrapi; @@ -18,7 +19,7 @@ interface IEntitiesRestoreStreamOptions { transaction?: Transaction; } -const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { +export const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { const { strapi, updateMappingTable, transaction } = options; const query = queries.entity.createEntityQuery(strapi); @@ -31,48 +32,6 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { const { create, getDeepPopulateComponentLikeQuery } = query(type); const contentType = strapi.getModel(type); - let cType: - | Schema.ContentType - | Schema.Component - | ((...opts: any[]) => Schema.ContentType | Schema.Component) = contentType; - - /** - * Resolve the component UID of an entity's attribute based - * on a given path (components & dynamic zones only) - */ - const resolveType = (paths: string[]): Common.UID.Schema | undefined => { - let value: unknown = data; - - for (const path of paths) { - value = get(path, value); - - // Needed when the value of cType should be computed - // based on the next value (eg: dynamic zones) - if (typeof cType === 'function') { - cType = cType(value); - } - - if (path in cType.attributes) { - const attribute = cType.attributes[path]; - - if (attribute.type === 'component') { - cType = strapi.getModel(attribute.component); - } - - if (attribute.type === 'dynamiczone') { - cType = ({ __component }: { __component: Common.UID.Component }) => - strapi.getModel(__component); - } - } - } - - if ('uid' in cType) { - return cType.uid; - } - - return undefined; - }; - try { const created = await create({ data, @@ -89,7 +48,7 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { // update the mapping the table accordingly diffs.forEach((diff) => { if (diff.kind === 'modified' && last(diff.path) === 'id') { - const target = resolveType(diff.path); + const target = resolveComponentUID({ paths: diff.path, data, contentType, strapi }); // If no type is found for the given path, then ignore the diff if (!target) { @@ -114,5 +73,3 @@ const createEntitiesWriteStream = (options: IEntitiesRestoreStreamOptions) => { }, }); }; - -export { createEntitiesWriteStream }; diff --git a/packages/core/data-transfer/src/utils/__tests__/components.test.ts b/packages/core/data-transfer/src/utils/__tests__/components.test.ts new file mode 100644 index 0000000000..476c00768f --- /dev/null +++ b/packages/core/data-transfer/src/utils/__tests__/components.test.ts @@ -0,0 +1,118 @@ +import type { LoadedStrapi, Schema } from '@strapi/types'; +import { resolveComponentUID } from '../components'; + +const baseContentType: Schema.ContentType = { + collectionName: 'test', + info: { + singularName: 'test', + pluralName: 'tests', + displayName: 'Test', + }, + attributes: { + // To fill in the different tests + }, + options: { + draftAndPublish: false, + }, + kind: 'collectionType', + modelType: 'contentType', + modelName: 'user', + uid: 'api::test.test', + globalId: 'Test', +}; + +describe('resolveComponentUID', () => { + const uid = 'test.test'; + + it('should return the component UID when the path matches a repeatable component', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + relsRepeatable: { + type: 'component', + repeatable: true, + component: uid, + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels_repeatables', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['relsRepeatable', '0', 'id']; + + const data = { + relsRepeatable: [{ id: 1, title: 'test' }], + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); + + it('should return the component UID when the path matches a single component', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + rels: { + type: 'component', + repeatable: false, + component: uid, + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['rels', 'id']; + + const data = { + rels: { id: 1, title: 'test' }, + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); + + it('should return the component UID when the path matches a dynamic zone', () => { + const contentType: Schema.ContentType | Schema.Component = { + ...baseContentType, + attributes: { + dz: { + type: 'dynamiczone', + components: [uid], + }, + }, + }; + const strapi = { + getModel: jest.fn().mockReturnValueOnce({ + collectionName: 'components_test_rels', + attributes: { + // doesn't matter + }, + uid, + }), + } as unknown as LoadedStrapi; + const paths = ['dz', '0', 'id']; + + const data = { + dz: [{ __component: 'test.rels', id: 1, title: 'test' }], + }; + + const expectedUID = resolveComponentUID({ paths, strapi, data, contentType }); + + expect(expectedUID).toEqual(uid); + }); +}); diff --git a/packages/core/data-transfer/src/utils/components.ts b/packages/core/data-transfer/src/utils/components.ts index 5be4244113..46f0e34471 100644 --- a/packages/core/data-transfer/src/utils/components.ts +++ b/packages/core/data-transfer/src/utils/components.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import { has, omit, pipe, assign } from 'lodash/fp'; +import { get, has, omit, pipe, assign } from 'lodash/fp'; import { contentTypes as contentTypesUtils, mapAsync, errors } from '@strapi/utils'; -import type { Attribute, Common, Schema, Utils, EntityService } from '@strapi/types'; +import type { Attribute, Common, Schema, Utils, EntityService, LoadedStrapi } from '@strapi/types'; type LoadedComponents = Attribute.GetValues< TUID, @@ -596,6 +596,56 @@ const cloneComponent = async ( return strapi.query(uid).clone(data.id, { data: transform(data) }); }; +/** + * Resolve the component UID of an entity's attribute based + * on a given path (components & dynamic zones only) + */ +const resolveComponentUID = ({ + paths, + strapi, + data, + contentType, +}: { + paths: string[]; + strapi: LoadedStrapi; + data: any; + contentType: Schema.ContentType; +}): Common.UID.Schema | undefined => { + let value: unknown = data; + let cType: + | Schema.ContentType + | Schema.Component + | ((...opts: any[]) => Schema.ContentType | Schema.Component) = contentType; + for (const path of paths) { + value = get(path, value); + + // Needed when the value of cType should be computed + // based on the next value (eg: dynamic zones) + if (typeof cType === 'function') { + cType = cType(value); + } + + if (path in cType.attributes) { + const attribute: Attribute.Any = cType.attributes[path]; + + if (attribute.type === 'component') { + cType = strapi.getModel(attribute.component); + } + + if (attribute.type === 'dynamiczone') { + cType = ({ __component }: { __component: Common.UID.Component }) => + strapi.getModel(__component); + } + } + } + + if ('uid' in cType) { + return cType.uid; + } + + return undefined; +}; + export { omitComponentData, getComponents, @@ -604,4 +654,5 @@ export { deleteComponents, deleteComponent, cloneComponents, + resolveComponentUID, }; From 0be352b4e0f45a2234ecbc2b8ab4071846984cc4 Mon Sep 17 00:00:00 2001 From: Christian Capeans Date: Thu, 21 Mar 2024 13:44:35 +0100 Subject: [PATCH 07/22] fix: remove unnecessary imports --- .../local-destination/strategies/restore/entities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts index c44adbdb6a..ddcd8ba136 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/entities.ts @@ -1,7 +1,7 @@ import { Writable } from 'stream'; -import type { LoadedStrapi, Common, Schema, Attribute } from '@strapi/types'; +import type { LoadedStrapi, Common } from '@strapi/types'; -import { get, last } from 'lodash/fp'; +import { last } from 'lodash/fp'; import { ProviderTransferError } from '../../../../../errors/providers'; import type { IEntity, Transaction } from '../../../../../../types'; From 1e46ca925484dd19eea4cd25f16982eb69881043 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 25 Mar 2024 13:59:41 +0100 Subject: [PATCH 08/22] fix(admin): show purchase pages also with Cloud plans and change message (#19855) --- packages/core/admin/admin/src/constants.ts | 8 ++++---- .../admin/src/pages/Settings/pages/PurchaseAuditLogs.tsx | 2 +- .../src/pages/Settings/pages/PurchaseReviewWorkflows.tsx | 2 +- .../src/pages/Settings/pages/PurchaseSingleSignOn.tsx | 2 +- packages/core/admin/admin/src/translations/en.json | 6 +++--- .../admin/src/pages/PurchaseContentReleases.tsx | 2 +- .../core/content-releases/admin/src/translations/en.json | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/admin/admin/src/constants.ts b/packages/core/admin/admin/src/constants.ts index e5e2b08f03..4aec537df9 100644 --- a/packages/core/admin/admin/src/constants.ts +++ b/packages/core/admin/admin/src/constants.ts @@ -155,7 +155,7 @@ export const SETTINGS_LINKS_CE = (): SettingsMenu => ({ to: '/settings/transfer-tokens?sort=name:ASC', id: 'transfer-tokens', }, - // If the Enterprise feature is not enabled and if the config doesn't disable it, we promote the Enterprise feature by displaying them in the settings menu. + // If the Enterprise/Cloud feature is not enabled and if the config doesn't disable it, we promote the Enterprise/Cloud feature by displaying them in the settings menu. // Disable this by adding "promoteEE: false" to your `./config/admin.js` file ...(!window.strapi.features.isEnabled(window.strapi.features.SSO) && window.strapi?.flags?.promoteEE @@ -163,7 +163,7 @@ export const SETTINGS_LINKS_CE = (): SettingsMenu => ({ { intlLabel: { id: 'Settings.sso.title', defaultMessage: 'Single Sign-On' }, to: '/settings/purchase-single-sign-on', - id: 'sso', + id: 'sso-purchase-page', lockIcon: true, }, ] @@ -178,7 +178,7 @@ export const SETTINGS_LINKS_CE = (): SettingsMenu => ({ defaultMessage: 'Review Workflows', }, to: '/settings/purchase-review-workflows', - id: 'review-workflows', + id: 'review-workflows-purchase-page', lockIcon: true, }, ] @@ -203,7 +203,7 @@ export const SETTINGS_LINKS_CE = (): SettingsMenu => ({ { intlLabel: { id: 'global.auditLogs', defaultMessage: 'Audit Logs' }, to: '/settings/purchase-audit-logs', - id: 'auditLogs', + id: 'auditLogs-purchase-page', lockIcon: true, }, ] diff --git a/packages/core/admin/admin/src/pages/Settings/pages/PurchaseAuditLogs.tsx b/packages/core/admin/admin/src/pages/Settings/pages/PurchaseAuditLogs.tsx index 1f2a7a2e58..9f7d65f0ac 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/PurchaseAuditLogs.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/PurchaseAuditLogs.tsx @@ -22,7 +22,7 @@ const PurchaseAuditLogs = () => { content={formatMessage({ id: 'Settings.permissions.auditLogs.not-available', defaultMessage: - 'Audit Logs is only available as part of the Enterprise Edition. Upgrade to get a searchable and filterable display of all activities.', + 'Audit Logs is only available as part of a paid plan. Upgrade to get a searchable and filterable display of all activities.', })} action={ { content={formatMessage({ id: 'Settings.review-workflows.not-available', defaultMessage: - 'Review Workflows is only available as part of the Enterprise Edition. Upgrade to create and manage workflows.', + 'Review Workflows is only available as part of a paid plan. Upgrade to create and manage workflows.', })} action={ { content={formatMessage({ id: 'Settings.sso.not-available', defaultMessage: - 'SSO is only available as part of the Enterprise Edition. Upgrade to configure additional sign-in & sign-up methods for your administration panel.', + 'SSO is only available as part of a paid plan. Upgrade to configure additional sign-in & sign-up methods for your administration panel.', })} action={ { content={formatMessage({ id: 'content-releases.pages.PurchaseRelease.not-available', defaultMessage: - 'Releases is only available as part of the Enterprise Edition. Upgrade to create and manage releases.', + 'Releases is only available as part of a paid plan. Upgrade to create and manage releases.', })} action={ Date: Mon, 25 Mar 2024 13:45:37 -0700 Subject: [PATCH 09/22] chore(deps): bump @koa/cors from 3.4.3 to 5.0.0 fixes https://www.npmjs.com/advisories/1095223 --- packages/core/strapi/package.json | 2 +- packages/core/types/package.json | 2 +- yarn.lock | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/core/strapi/package.json b/packages/core/strapi/package.json index 078506d4e8..7138bad2a1 100644 --- a/packages/core/strapi/package.json +++ b/packages/core/strapi/package.json @@ -111,7 +111,7 @@ "watch": "pack-up watch" }, "dependencies": { - "@koa/cors": "3.4.3", + "@koa/cors": "5.0.0", "@koa/router": "10.1.1", "@strapi/admin": "4.21.0", "@strapi/content-releases": "4.21.0", diff --git a/packages/core/types/package.json b/packages/core/types/package.json index f9737946c8..ff59b56ed8 100644 --- a/packages/core/types/package.json +++ b/packages/core/types/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@casl/ability": "6.5.0", - "@koa/cors": "3.4.3", + "@koa/cors": "5.0.0", "@koa/router": "10.1.1", "@strapi/database": "4.21.0", "@strapi/logger": "4.21.0", diff --git a/yarn.lock b/yarn.lock index 2e1951d46a..142fa54bcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4136,7 +4136,16 @@ __metadata: languageName: node linkType: hard -"@koa/cors@npm:3.4.3, @koa/cors@npm:^3.1.0": +"@koa/cors@npm:5.0.0": + version: 5.0.0 + resolution: "@koa/cors@npm:5.0.0" + dependencies: + vary: "npm:^1.1.2" + checksum: 3a0e32fbc422a5f9a41540ce3b7499d46073ddb0e4e851394a74bac5ecd0eaa1f24a8f189b7bd6a50c5863788ae6945c52d990edf99fdd2151a4404f266fe2e7 + languageName: node + linkType: hard + +"@koa/cors@npm:^3.1.0": version: 3.4.3 resolution: "@koa/cors@npm:3.4.3" dependencies: @@ -8767,7 +8776,7 @@ __metadata: version: 0.0.0-use.local resolution: "@strapi/strapi@workspace:packages/core/strapi" dependencies: - "@koa/cors": "npm:3.4.3" + "@koa/cors": "npm:5.0.0" "@koa/router": "npm:10.1.1" "@strapi/admin": "npm:4.21.0" "@strapi/content-releases": "npm:4.21.0" @@ -8871,7 +8880,7 @@ __metadata: resolution: "@strapi/types@workspace:packages/core/types" dependencies: "@casl/ability": "npm:6.5.0" - "@koa/cors": "npm:3.4.3" + "@koa/cors": "npm:5.0.0" "@koa/router": "npm:10.1.1" "@strapi/database": "npm:4.21.0" "@strapi/logger": "npm:4.21.0" From 63c0d8309704e79aee05387e5689ba473f5a29bf Mon Sep 17 00:00:00 2001 From: DMehaffy Date: Tue, 26 Mar 2024 00:55:55 -0700 Subject: [PATCH 10/22] chore(deps): bump sanitize-html (and types) from 2.11.0 to 2.13.0 (#19922) fixes https://www.npmjs.com/advisories/1096639 --- packages/core/admin/package.json | 4 ++-- yarn.lock | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json index 6bb339af0b..e1b66bf1c0 100644 --- a/packages/core/admin/package.json +++ b/packages/core/admin/package.json @@ -152,7 +152,7 @@ "read-pkg-up": "7.0.1", "resolve-from": "5.0.0", "rimraf": "3.0.2", - "sanitize-html": "2.11.0", + "sanitize-html": "2.13.0", "semver": "7.5.4", "sift": "16.0.1", "slate": "0.94.1", @@ -192,7 +192,7 @@ "@types/passport-local": "1.0.36", "@types/pluralize": "0.0.32", "@types/react-window": "1.8.8", - "@types/sanitize-html": "2.9.5", + "@types/sanitize-html": "2.11.0", "@types/webpack-bundle-analyzer": "4.6.3", "@types/webpack-hot-middleware": "2.25.9", "msw": "1.3.0", diff --git a/yarn.lock b/yarn.lock index 2e1951d46a..8e4f3caf39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7827,7 +7827,7 @@ __metadata: "@types/passport-local": "npm:1.0.36" "@types/pluralize": "npm:0.0.32" "@types/react-window": "npm:1.8.8" - "@types/sanitize-html": "npm:2.9.5" + "@types/sanitize-html": "npm:2.11.0" "@types/webpack-bundle-analyzer": "npm:4.6.3" "@types/webpack-hot-middleware": "npm:2.25.9" "@vitejs/plugin-react-swc": "npm:3.5.0" @@ -7906,7 +7906,7 @@ __metadata: read-pkg-up: "npm:7.0.1" resolve-from: "npm:5.0.0" rimraf: "npm:3.0.2" - sanitize-html: "npm:2.11.0" + sanitize-html: "npm:2.13.0" semver: "npm:7.5.4" sift: "npm:16.0.1" slate: "npm:0.94.1" @@ -10425,12 +10425,12 @@ __metadata: languageName: node linkType: hard -"@types/sanitize-html@npm:2.9.5": - version: 2.9.5 - resolution: "@types/sanitize-html@npm:2.9.5" +"@types/sanitize-html@npm:2.11.0": + version: 2.11.0 + resolution: "@types/sanitize-html@npm:2.11.0" dependencies: htmlparser2: "npm:^8.0.0" - checksum: fd0afee5dac91aa2c42391f0c8c9254204f4ee1f10b902aa04e8f7809043d785e28af2732f75277ef09e46838013ad60abedb02ba1424b6218264f3333437fb5 + checksum: a901d55d31cd946a7fce0130cc7cf6bcf56602af9c87291be77d8149c60e7afc47c83ca74c67c2d84e6ba029fe9bbd6f14f89a8cb30fbd185766eebc5722c251 languageName: node linkType: hard @@ -27698,9 +27698,9 @@ __metadata: languageName: node linkType: hard -"sanitize-html@npm:2.11.0": - version: 2.11.0 - resolution: "sanitize-html@npm:2.11.0" +"sanitize-html@npm:2.13.0": + version: 2.13.0 + resolution: "sanitize-html@npm:2.13.0" dependencies: deepmerge: "npm:^4.2.2" escape-string-regexp: "npm:^4.0.0" @@ -27708,7 +27708,7 @@ __metadata: is-plain-object: "npm:^5.0.0" parse-srcset: "npm:^1.0.2" postcss: "npm:^8.3.11" - checksum: 452029f5b15ef6b41729f7f45ee853d020ed0859388534bd9b959d78bb0df6d9dcaff6103a8c16597a5a21ee63f00127ce387d16b7a6538174081abac9d34031 + checksum: 2805259492c2a5e6fcef5e7edc35a90a8d4bc4e0e844b23117986a8c23ea6345df111595396c3a72960fea01c337ccc4f693699782aea7c52839da3e25b7e23c languageName: node linkType: hard From 343968041bed1eb9d230115e687e9397b0f5fd50 Mon Sep 17 00:00:00 2001 From: Marc Roig Date: Tue, 26 Mar 2024 13:44:21 +0100 Subject: [PATCH 11/22] fix: relations lazy loading (#19932) --- .../core/content-manager/server/src/controllers/relations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/content-manager/server/src/controllers/relations.ts b/packages/core/content-manager/server/src/controllers/relations.ts index 8e8230db46..423af1fe65 100644 --- a/packages/core/content-manager/server/src/controllers/relations.ts +++ b/packages/core/content-manager/server/src/controllers/relations.ts @@ -280,8 +280,8 @@ export default { ordering: 'desc', } as any, { - page: ctx.request.query.page, - pageSize: ctx.request.query.pageSize, + page: 1, + pageSize: ids.length, } ); From dd543b13a3ba4b6b545851c24db5c6c2412e2187 Mon Sep 17 00:00:00 2001 From: Madhuri Sandbhor Date: Tue, 26 Mar 2024 14:30:44 +0100 Subject: [PATCH 12/22] Feat(releases): Bulk Release (#19891) * draft: bulk release injection zone added * chore: implement cm-api to strapi-app & description component renderer * feat: bulk delete action * Revert "draft: bulk release injection zone added" This reverts commit 2d9e8872e847327888d988009fa16f91b5f1c777. * feat: bulk unpublish action added * feat: default bulk publish action * fix: variable renaming * fix: on close of modal updated and refetched list on publish action completion * fix: removed IZ publish and unpublish modals, e2e tests added for default bulk actions * tests: e2e updated for publish button disabled state * fix: bug fixed on publishing with already published entries * fix: e2e tests * fix: reverted IZ components * feat: bulk release action added * update: check right permissions to show add to release button * update: bulk actions modal updated to accept content(modalbody+modalfooter), publish action and release actions updated accordingly * fix: types * test: bulk release e2e test added * fix: e2e test * fix: test case timeout added, notification component updated to not saved message --------- Co-authored-by: Josh <37798644+joshuaellis@users.noreply.github.com> --- .../content-releases/releases-page.spec.ts | 31 +++ .../components/BulkActions/Actions.tsx | 13 +- .../components/BulkActions/PublishAction.tsx | 171 ++++++------- packages/core/admin/admin/src/index.ts | 1 + .../src/components/CMReleasesContainer.tsx | 8 +- .../admin/src/components/ReleaseAction.tsx | 236 ++++++++++++++++++ .../core/content-releases/admin/src/index.ts | 10 + .../admin/src/services/release.ts | 19 ++ .../admin/src/translations/en.json | 8 + .../src/features/Notifications.tsx | 17 +- 10 files changed, 398 insertions(+), 116 deletions(-) create mode 100644 packages/core/content-releases/admin/src/components/ReleaseAction.tsx diff --git a/e2e/tests/content-releases/releases-page.spec.ts b/e2e/tests/content-releases/releases-page.spec.ts index ae30ef5c40..b52393349d 100644 --- a/e2e/tests/content-releases/releases-page.spec.ts +++ b/e2e/tests/content-releases/releases-page.spec.ts @@ -91,4 +91,35 @@ describeOnCondition(edition === 'EE')('Releases page', () => { await page.getByRole('link', { name: 'Releases' }).click(); await expect(page.getByRole('link', { name: `${newReleaseName}` })).toBeVisible(); }); + + test('A user should be able to perform bulk release on entries', async ({ page }) => { + await page.getByRole('link', { name: 'Content Manager' }).click(); + + await expect(page).toHaveTitle('Content Manager'); + await expect(page.getByRole('heading', { name: 'Article' })).toBeVisible(); + const publishedItems = page.getByRole('gridcell', { name: 'published' }); + expect(publishedItems).toHaveCount(2); + const checkbox = page.getByRole('checkbox', { name: 'Select all entries' }); + + // Select all entries to release + await checkbox.check(); + const addToRelease = page.getByRole('button', { name: 'add to release' }); + await addToRelease.click(); + + // Wait for the add to release dialog to appear + await page + .getByRole('combobox', { + name: 'Select a release', + }) + .click(); + + await page.getByRole('option', { name: 'Trent Crimm: The Independent' }).click(); + const unpublishButton = page.getByText('unpublish', { exact: true }); + await unpublishButton.click(); + await page.getByText('continue').click(); + await page.getByText(/Successfully added to release./).waitFor({ + state: 'visible', + timeout: 5000, + }); + }); }); diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActions/Actions.tsx b/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActions/Actions.tsx index 7b581d2f4b..ef89f449a2 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActions/Actions.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActions/Actions.tsx @@ -8,7 +8,6 @@ import { DialogFooter, DialogProps, Flex, - ModalBody, ModalHeader, ModalLayout, Typography, @@ -23,6 +22,7 @@ import { } from '@strapi/helper-plugin'; import { Check, ExclamationMarkCircle, Trash } from '@strapi/icons'; import { Contracts } from '@strapi/plugin-content-manager/_internal/shared'; +import { Common } from '@strapi/types'; import { AxiosError, AxiosResponse } from 'axios'; import { useIntl } from 'react-intl'; import { useQueryClient, useMutation } from 'react-query'; @@ -81,8 +81,7 @@ interface NotificationOptions { interface ModalOptions { type: 'modal'; title: string; - content: React.ReactNode; - footer: React.ComponentType<{ onClose: () => void }> | React.ReactNode; + content: React.ComponentType<{ onClose: () => void }>; onClose?: () => void; } @@ -95,7 +94,7 @@ const BulkActionsRenderer = () => { const { selectedEntries } = useTableContext(); const { slug, collectionType } = useParams<{ - slug: string; + slug: Common.UID.ContentType; collectionType: string; }>(); @@ -277,8 +276,7 @@ const BulkActionModal = ({ isOpen, title, onClose, - footer: Footer, - content, + content: Content, onModalClose, }: BulkActionModalProps) => { const id = React.useId(); @@ -302,8 +300,7 @@ const BulkActionModal = ({ {title} - {content} - <>{typeof Footer === 'function' ?