From 030161746a0587aa482fa35eef449a74db435119 Mon Sep 17 00:00:00 2001 From: Jamie Howard <48524071+jhoward1994@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:51:17 +0000 Subject: [PATCH 01/14] fix(content-manager): send locale when deleting i18n single type (#19629) --- .../components/ContentTypeFormWrapper.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/ContentTypeFormWrapper.tsx b/packages/core/admin/admin/src/content-manager/components/ContentTypeFormWrapper.tsx index 2455790cd0..8ee5ffd459 100644 --- a/packages/core/admin/admin/src/content-manager/components/ContentTypeFormWrapper.tsx +++ b/packages/core/admin/admin/src/content-manager/components/ContentTypeFormWrapper.tsx @@ -88,7 +88,13 @@ const ContentTypeFormWrapper = ({ const { setCurrentStep } = useGuidedTour(); const { trackUsage } = useTracking(); const { push, replace } = useHistory(); - const [{ query, rawQuery }] = useQueryParams(); + const [{ query, rawQuery }] = useQueryParams<{ + plugins?: { + i18n?: { + locale?: string; + }; + }; + }>(); const dispatch = useTypedDispatch(); const { componentsDataStructure, contentTypeDataStructure, data, isLoading, status } = useTypedSelector((state) => state['content-manager_editViewCrudReducer']); @@ -252,8 +258,12 @@ const ContentTypeFormWrapper = ({ try { trackUsage('willDeleteEntry', trackerProperty); + const locale = query?.plugins?.i18n?.locale; + const params = isSingleType && locale ? { locale } : {}; + const { data } = await del( - `/content-manager/${collectionType}/${slug}/${id}` + `/content-manager/${collectionType}/${slug}/${id}`, + { params } ); toggleNotification({ From 80a18ebd59f47cacc8e68984612e16b49689283f Mon Sep 17 00:00:00 2001 From: Madhuri Sandbhor Date: Thu, 29 Feb 2024 10:13:49 +0100 Subject: [PATCH 02/14] fix: delete selected timezone value (#19628) --- .../content-releases/admin/src/components/ReleaseModal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx index 9b44347fe8..29e3e69633 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseModal.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseModal.tsx @@ -298,6 +298,9 @@ const TimezoneComponent = ({ timezoneOptions }: { timezoneOptions: ITimezoneOpti onChange={(timezone) => { setFieldValue('timezone', timezone); }} + onTextValueChange={(timezone) => { + setFieldValue('timezone', timezone); + }} onClear={() => { setFieldValue('timezone', ''); }} From 710a9e5658e886ed0bfe2c2751d695381e3b2a89 Mon Sep 17 00:00:00 2001 From: Madhuri Sandbhor Date: Thu, 29 Feb 2024 10:14:07 +0100 Subject: [PATCH 03/14] fix: extra padding removed, background color added for delete action (#19627) --- .../admin/src/pages/ReleaseDetailsPage.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 0c06ec7e07..4a7ca2b70f 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -78,13 +78,20 @@ const ReleaseInfoWrapper = styled(Flex)` border-top: 1px solid ${({ theme }) => theme.colors.neutral150}; `; -const StyledMenuItem = styled(Menu.Item)<{ disabled?: boolean }>` +const StyledMenuItem = styled(Menu.Item)<{ + disabled?: boolean; + variant?: 'neutral' | 'danger'; +}>` svg path { fill: ${({ theme, disabled }) => disabled && theme.colors.neutral500}; } span { color: ${({ theme, disabled }) => disabled && theme.colors.neutral500}; } + + &:hover { + background: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}100`]}; + } `; const PencilIcon = styled(Pencil)` @@ -387,14 +394,7 @@ export const ReleaseDetailsLayout = ({ width="100%" > - + {formatMessage({ @@ -404,15 +404,12 @@ export const ReleaseDetailsLayout = ({ - - + + {formatMessage({ From d10040847b91742ccb8083938399b63ffa289c7a Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 29 Feb 2024 14:32:37 +0100 Subject: [PATCH 04/14] fix: api and transfer token lifespan select lists work with all durations --- .../src/api/config/controllers/config.js | 7 ++ .../template/src/api/config/routes/config.js | 8 +++ .../template/src/create-transfer-token.js | 27 ++++++++ e2e/app-template/template/src/index.js | 24 +------ e2e/constants.js | 2 + e2e/scripts/dts-import.js | 13 ++++ e2e/tests/admin/transfer/tokens.spec.ts | 65 +++++++++++++++++++ e2e/utils/shared.ts | 20 +++++- .../pages/ApiTokens/EditView/EditViewPage.tsx | 6 +- .../pages/TransferTokens/EditView.tsx | 4 +- .../admin/server/src/services/api-token.ts | 25 +++++-- .../server/src/services/transfer/token.ts | 34 ++++++---- .../core/admin/shared/contracts/api-token.ts | 2 +- .../core/admin/shared/contracts/transfer.ts | 4 +- 14 files changed, 192 insertions(+), 49 deletions(-) create mode 100644 e2e/app-template/template/src/create-transfer-token.js create mode 100644 e2e/tests/admin/transfer/tokens.spec.ts diff --git a/e2e/app-template/template/src/api/config/controllers/config.js b/e2e/app-template/template/src/api/config/controllers/config.js index 25d061fd2f..97301592ed 100644 --- a/e2e/app-template/template/src/api/config/controllers/config.js +++ b/e2e/app-template/template/src/api/config/controllers/config.js @@ -1,3 +1,5 @@ +const { createTestTransferToken } = require('../../../create-transfer-token'); + module.exports = { rateLimitEnable(ctx) { const { value } = ctx.request.body; @@ -6,6 +8,11 @@ module.exports = { configService.rateLimitEnable(value); + ctx.send(200); + }, + async resetTransferToken(ctx) { + await createTestTransferToken(strapi); + ctx.send(200); }, }; diff --git a/e2e/app-template/template/src/api/config/routes/config.js b/e2e/app-template/template/src/api/config/routes/config.js index 3653f076f1..3aee096f85 100644 --- a/e2e/app-template/template/src/api/config/routes/config.js +++ b/e2e/app-template/template/src/api/config/routes/config.js @@ -8,5 +8,13 @@ module.exports = { auth: false, }, }, + { + method: 'POST', + path: '/config/resettransfertoken', + handler: 'config.resetTransferToken', + config: { + auth: false, + }, + }, ], }; diff --git a/e2e/app-template/template/src/create-transfer-token.js b/e2e/app-template/template/src/create-transfer-token.js new file mode 100644 index 0000000000..9a1a815335 --- /dev/null +++ b/e2e/app-template/template/src/create-transfer-token.js @@ -0,0 +1,27 @@ +const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants'); + +/** + * Make sure the test transfer token exists in the database + * @param {Strapi.Strapi} strapi + * @returns {Promise} + */ +const createTestTransferToken = async (strapi) => { + const { token: transferTokenService } = strapi.admin.services.transfer; + + const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY); + const exists = await transferTokenService.exists({ accessKey: accessKeyHash }); + + if (!exists) { + await transferTokenService.create({ + name: 'TestToken', + description: 'Transfer token used to seed the e2e database', + lifespan: null, + permissions: ['push'], + accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY, + }); + } +}; + +module.exports = { + createTestTransferToken, +}; diff --git a/e2e/app-template/template/src/index.js b/e2e/app-template/template/src/index.js index 8320d5ff43..4d9eec9a85 100644 --- a/e2e/app-template/template/src/index.js +++ b/e2e/app-template/template/src/index.js @@ -1,4 +1,4 @@ -const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants'); +const { createTestTransferToken } = require('./create-transfer-token'); module.exports = { /** @@ -23,25 +23,3 @@ module.exports = { await createTestTransferToken(strapi); }, }; - -/** - * Make sure the test transfer token exists in the database - * @param {Strapi.Strapi} strapi - * @returns {Promise} - */ -const createTestTransferToken = async (strapi) => { - const { token: transferTokenService } = strapi.admin.services.transfer; - - const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY); - const exists = await transferTokenService.exists({ accessKey: accessKeyHash }); - - if (!exists) { - await transferTokenService.create({ - name: 'TestToken', - description: 'Transfer token used to seed the e2e database', - lifespan: null, - permissions: ['push'], - accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY, - }); - } -}; diff --git a/e2e/constants.js b/e2e/constants.js index 714f8e747a..0ea248aa05 100644 --- a/e2e/constants.js +++ b/e2e/constants.js @@ -4,6 +4,8 @@ const ALLOWED_CONTENT_TYPES = [ 'admin::user', 'admin::role', 'admin::permission', + 'admin::api-token', + 'admin::transfer-token', 'api::article.article', 'api::author.author', 'api::homepage.homepage', diff --git a/e2e/scripts/dts-import.js b/e2e/scripts/dts-import.js index 91bca192c3..66e923a828 100644 --- a/e2e/scripts/dts-import.js +++ b/e2e/scripts/dts-import.js @@ -48,6 +48,19 @@ export const resetDatabaseAndImportDataFromPath = async (filePath) => { engine.diagnostics.onDiagnostic(console.log); + try { + // reset the transfer token to allow the transfer if it's been wiped (that is, not included in previous import data) + const res = await fetch( + `http://127.0.0.1:${process.env.PORT ?? 1337}/api/config/resettransfertoken`, + { + method: 'POST', + } + ); + } catch (err) { + console.error('Token reset failed.' + JSON.stringify(err, null, 2)); + process.exit(1); + } + try { await engine.transfer(); } catch { diff --git a/e2e/tests/admin/transfer/tokens.spec.ts b/e2e/tests/admin/transfer/tokens.spec.ts new file mode 100644 index 0000000000..0fdc05e241 --- /dev/null +++ b/e2e/tests/admin/transfer/tokens.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { login } from '../../../utils/login'; +import { resetDatabaseAndImportDataFromPath } from '../../../scripts/dts-import'; +import { navToHeader, delay } from '../../../utils/shared'; + +const createTransferToken = async (page, tokenName, duration, type) => { + await navToHeader( + page, + ['Settings', 'Transfer Tokens', 'Create new Transfer Token'], + 'Create Transfer Token' + ); + + await page.getByLabel('Name*').click(); + await page.getByLabel('Name*').fill(tokenName); + + await page.getByLabel('Token duration').click(); + await page.getByRole('option', { name: duration }).click(); + + await page.getByLabel('Token type').click(); + await page.getByRole('option', { name: type }).click(); + + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText(/copy this token/)).toBeVisible(); + await expect(page.getByText('Expiration date:')).toBeVisible(); +}; + +test.describe('Transfer Tokens', () => { + test.beforeEach(async ({ page }) => { + await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar'); + await page.goto('/admin'); + await login({ page }); + }); + + // Test token creation + const testCases = [ + ['30-day push token', '30 days', 'Push'], + ['30-day pull token', '30 days', 'Pull'], + ['30-day full-access token', '30 days', 'Full access'], + // if push+pull work generally that's good enough for e2e + ['7-day token', '7 days', 'Full access'], + ['90-day token', '90 days', 'Full access'], + ['unlimited token', 'Unlimited', 'Full access'], + ]; + for (const [name, duration, type] of testCases) { + test(`A user should be able to create a ${name}`, async ({ page }) => { + await createTransferToken(page, name, duration, type); + }); + } + + test('Created tokens list page should be correct', async ({ page }) => { + await createTransferToken(page, 'my test token', 'unlimited', 'Full access'); + + // if we don't wait until createdAt is at least 1s, we see "NaN" for the timestamp + // TODO: fix the bug and remove this + await page.waitForTimeout(1100); + + await navToHeader(page, ['Settings', 'Transfer Tokens'], 'Transfer Tokens'); + + const row = page.getByRole('gridcell', { name: 'my test token', exact: true }); + await expect(row).toBeVisible(); + await expect(page.getByText(/\d+ (second|minute)s? ago/)).toBeVisible(); + // TODO: expand on this test, it could check edit and delete icons + }); +}); diff --git a/e2e/utils/shared.ts b/e2e/utils/shared.ts index 99cbbdc302..05e00d5b24 100644 --- a/e2e/utils/shared.ts +++ b/e2e/utils/shared.ts @@ -1,7 +1,25 @@ -import { test } from '@playwright/test'; +import { test, Page, expect } from '@playwright/test'; /** * Execute a test suite only if the condition is true */ export const describeOnCondition = (shouldDescribe: boolean) => shouldDescribe ? test.describe : test.describe.skip; + +/** + * Navigate to a page and confirm the header, awaiting each step + */ +export const navToHeader = async (page: Page, navItems: string[], headerText: string) => { + for (const navItem of navItems) { + // This does not use getByRole because sometimes "Settings" is "Settings 1" if there's a badge notification + // BUT if we don't match exact it conflicts with "Advanceed Settings" + // As a workaround, we implement our own startsWith with page.locator + const item = page.locator(`role=link[name^="${navItem}"]`); + await expect(item).toBeVisible(); + await item.click(); + } + + const header = page.getByRole('heading', { name: headerText, exact: true }); + await expect(header).toBeVisible(); + return header; +}; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx index 9a8c8e669e..b18563e8f9 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx @@ -201,8 +201,8 @@ export const EditView = () => { if (isCreating) { const res = await createToken({ ...body, - // in case a token has a lifespan of "unlimited" the API only accepts zero as a number - lifespan: body.lifespan === '0' ? parseInt(body.lifespan) : null, + // lifespan must be "null" for unlimited (0 would mean instantly expired and isn't accepted) + lifespan: body?.lifespan || null, permissions: body.type === 'custom' ? state.selectedActions : null, }); @@ -338,7 +338,7 @@ export const EditView = () => { name: apiToken?.name || '', description: apiToken?.description || '', type: apiToken?.type, - lifespan: apiToken?.lifespan ? apiToken.lifespan.toString() : apiToken?.lifespan, + lifespan: apiToken?.lifespan, }} enableReinitialize onSubmit={(body, actions) => handleSubmit(body, actions)} diff --git a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx index 2e9aa94d87..403dd3b6b8 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx @@ -150,6 +150,8 @@ const EditView = () => { if (isCreating) { const res = await createToken({ ...body, + // lifespan must be "null" for unlimited (0 would mean instantly expired and isn't accepted) + lifespan: body?.lifespan || null, permissions, }); @@ -251,7 +253,7 @@ const EditView = () => { { name: transferToken?.name || '', description: transferToken?.description || '', - lifespan: transferToken?.lifespan ?? null, + lifespan: transferToken?.lifespan || null, /** * We need to cast the permissions to satisfy the type for `permissions` * in the request body incase we don't have a transferToken and instead diff --git a/packages/core/admin/server/src/services/api-token.ts b/packages/core/admin/server/src/services/api-token.ts index 9f85b46ae9..eaff6a7895 100644 --- a/packages/core/admin/server/src/services/api-token.ts +++ b/packages/core/admin/server/src/services/api-token.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { omit, difference, isNil, isEmpty, map, isArray, uniq } from 'lodash/fp'; +import { omit, difference, isNil, isEmpty, map, isArray, uniq, isNumber } from 'lodash/fp'; import { errors } from '@strapi/utils'; import type { Update, ApiToken, ApiTokenBody } from '../../../shared/contracts/api-token'; import constants from './constants'; @@ -61,14 +61,25 @@ const assertCustomTokenPermissionsValidity = ( }; /** - * Assert that a token's lifespan is valid + * Check if a token's lifespan is valid */ -const assertValidLifespan = (lifespan: ApiTokenBody['lifespan']) => { +const isValidLifespan = (lifespan: unknown) => { if (isNil(lifespan)) { - return; + return true; } - if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan as number)) { + if (!isNumber(lifespan) || !Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) { + return false; + } + + return true; +}; + +/** + * Assert that a token's lifespan is valid + */ +const assertValidLifespan = (lifespan: unknown) => { + if (!isValidLifespan(lifespan)) { throw new ValidationError( `lifespan must be one of the following values: ${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}` @@ -138,14 +149,14 @@ const hash = (accessKey: string) => { const getExpirationFields = (lifespan: ApiTokenBody['lifespan']) => { // it must be nil or a finite number >= 0 - const isValidNumber = Number.isFinite(lifespan) && (lifespan as number) > 0; + const isValidNumber = isNumber(lifespan) && Number.isFinite(lifespan) && lifespan > 0; if (!isValidNumber && !isNil(lifespan)) { throw new ValidationError('lifespan must be a positive number or null'); } return { lifespan: lifespan || null, - expiresAt: lifespan ? Date.now() + (lifespan as number) : null, + expiresAt: lifespan ? Date.now() + lifespan : null, }; }; diff --git a/packages/core/admin/server/src/services/transfer/token.ts b/packages/core/admin/server/src/services/transfer/token.ts index 1268d5eeef..b9b96cffc4 100644 --- a/packages/core/admin/server/src/services/transfer/token.ts +++ b/packages/core/admin/server/src/services/transfer/token.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import assert from 'assert'; -import { map, isArray, omit, uniq, isNil, difference, isEmpty } from 'lodash/fp'; +import { map, isArray, omit, uniq, isNil, difference, isEmpty, isNumber } from 'lodash/fp'; import { errors } from '@strapi/utils'; import '@strapi/types'; import constants from '../constants'; @@ -79,7 +79,7 @@ const create = async (attributes: TokenCreatePayload): Promise => delete attributes.accessKey; assertTokenPermissionsValidity(attributes); - assertValidLifespan(attributes); + assertValidLifespan(attributes.lifespan); const result = (await strapi.db.transaction(async () => { const transferToken = await strapi.query(TRANSFER_TOKEN_UID).create({ @@ -131,7 +131,7 @@ const update = async ( } assertTokenPermissionsValidity(attributes); - assertValidLifespan(attributes); + assertValidLifespan(attributes.lifespan); return strapi.db.transaction(async () => { const updatedToken = await strapi.query(TRANSFER_TOKEN_UID).update({ @@ -281,11 +281,9 @@ const regenerate = async (id: string | number): Promise => { }; }; -const getExpirationFields = ( - lifespan: number | null -): { lifespan: null | number; expiresAt: null | number } => { +const getExpirationFields = (lifespan: TransferToken['lifespan']) => { // it must be nil or a finite number >= 0 - const isValidNumber = Number.isFinite(lifespan) && lifespan !== null && lifespan > 0; + const isValidNumber = isNumber(lifespan) && Number.isFinite(lifespan) && lifespan > 0; if (!isValidNumber && !isNil(lifespan)) { throw new ValidationError('lifespan must be a positive number or null'); } @@ -359,14 +357,28 @@ const assertTokenPermissionsValidity = (attributes: TokenUpdatePayload) => { }; /** - * Assert that a token's lifespan is valid + * Check if a token's lifespan is valid */ -const assertValidLifespan = ({ lifespan }: { lifespan?: TransferToken['lifespan'] }) => { +const isValidLifespan = (lifespan: unknown) => { if (isNil(lifespan)) { - return; + return true; } - if (!Object.values(constants.TRANSFER_TOKEN_LIFESPANS).includes(lifespan)) { + if ( + !isNumber(lifespan) || + !Object.values(constants.TRANSFER_TOKEN_LIFESPANS).includes(lifespan) + ) { + return false; + } + + return true; +}; + +/** + * Assert that a token's lifespan is valid + */ +const assertValidLifespan = (lifespan: unknown) => { + if (!isValidLifespan(lifespan)) { throw new ValidationError( `lifespan must be one of the following values: ${Object.values(constants.TRANSFER_TOKEN_LIFESPANS).join(', ')}` diff --git a/packages/core/admin/shared/contracts/api-token.ts b/packages/core/admin/shared/contracts/api-token.ts index 4d4da32500..a7dc08f218 100644 --- a/packages/core/admin/shared/contracts/api-token.ts +++ b/packages/core/admin/shared/contracts/api-token.ts @@ -8,7 +8,7 @@ export type ApiToken = { expiresAt: string; id: Entity.ID; lastUsedAt: string | null; - lifespan: string | number; + lifespan: string | number | null; name: string; permissions: string[]; type: 'custom' | 'full-access' | 'read-only'; diff --git a/packages/core/admin/shared/contracts/transfer.ts b/packages/core/admin/shared/contracts/transfer.ts index 2362d200ad..1eb1312401 100644 --- a/packages/core/admin/shared/contracts/transfer.ts +++ b/packages/core/admin/shared/contracts/transfer.ts @@ -1,7 +1,7 @@ import { errors } from '@strapi/utils'; export interface TransferTokenPermission { - id: number | string; + id: number | `${number}`; action: 'push' | 'pull' | 'push-pull'; token: TransferToken | number; } @@ -12,7 +12,7 @@ export interface DatabaseTransferToken { description: string; accessKey: string; lastUsedAt?: number; - lifespan: number | null; + lifespan: string | number | null; expiresAt: number; permissions: TransferTokenPermission[]; } From df59595088d3062d60697af546128c2aa792f49b Mon Sep 17 00:00:00 2001 From: Mark Kaylor Date: Thu, 29 Feb 2024 14:54:03 +0100 Subject: [PATCH 05/14] fix(content-releases): remove argument passed to test render --- .../admin/src/components/tests/CMReleasesContainer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx b/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx index ffffe21844..6d9b4a5331 100644 --- a/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx +++ b/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx @@ -138,7 +138,7 @@ describe('CMReleasesContainer', () => { }) ); - render(); + render(); const informationBox = await screen.findByRole('complementary', { name: 'Releases' }); const release1 = await within(informationBox).findByText('01/01/2024 at 11:00 (UTC+01:00)'); From 93eda17d2ada24da9ee897406eb02bc37c1c7258 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:18:21 +0000 Subject: [PATCH 06/14] chore(helper-plugin)!: remove form (#19612) * chore!: remove form * chore: re-introduce form error focusing * chore: remove usage of Formik in auth * chore: fix test suite --- e2e/tests/admin/signup.spec.ts | 2 +- e2e/utils/login.ts | 2 +- .../{content-manager => }/components/Form.tsx | 34 +- .../components/FormInputs/Boolean.tsx | 1 + .../src/components/FormInputs/Checkbox.tsx | 34 ++ .../components/FormInputs/Date.tsx | 1 + .../components/FormInputs/DateTime.tsx | 1 + .../components/FormInputs/Email.tsx | 1 + .../components/FormInputs/Enumeration.tsx | 1 + .../components/FormInputs/Json.tsx | 1 + .../components/FormInputs/Number.tsx | 1 + .../components/FormInputs/Password.tsx | 1 + .../components/FormInputs/Renderer.tsx | 4 + .../components/FormInputs/String.tsx | 1 + .../components/FormInputs/Textarea.tsx | 1 + .../components/FormInputs/Time.tsx | 1 + .../components/FormInputs/types.ts | 26 +- .../ConfigurationForm/EditFieldForm.tsx | 4 +- .../components/ConfigurationForm/Fields.tsx | 6 +- .../components/ConfigurationForm/Form.tsx | 4 +- .../tests/EditFieldForm.test.tsx | 2 +- .../components/Relations/RelationInput.tsx | 2 +- .../admin/src/content-manager/exports.ts | 2 - .../hooks/useDocumentLayout.ts | 5 +- .../pages/ComponentConfigurationPage.tsx | 2 +- .../pages/EditConfigurationPage.tsx | 2 +- .../pages/EditView/EditViewPage.tsx | 2 +- .../EditView/components/DocumentActions.tsx | 2 +- .../FormInputs/BlocksInput/Blocks/Link.tsx | 2 +- .../FormInputs/BlocksInput/BlocksContent.tsx | 2 +- .../FormInputs/BlocksInput/BlocksEditor.tsx | 2 +- .../FormInputs/BlocksInput/BlocksInput.tsx | 4 +- .../BlocksInput/tests/BlocksInput.test.tsx | 2 +- .../FormInputs/Component/Initializer.tsx | 2 +- .../components/FormInputs/Component/Input.tsx | 2 +- .../FormInputs/Component/Repeatable.tsx | 6 +- .../DynamicZone/DynamicComponent.tsx | 6 +- .../DynamicZone/DynamicZoneLabel.tsx | 2 +- .../FormInputs/DynamicZone/Field.tsx | 2 +- .../tests/DynamicComponent.test.tsx | 2 +- .../DynamicZone/tests/Field.test.tsx | 2 +- .../EditView/components/FormInputs/UID.tsx | 5 +- .../components/FormInputs/Wysiwyg/Editor.tsx | 4 +- .../components/FormInputs/Wysiwyg/Field.tsx | 4 +- .../FormInputs/Wysiwyg/tests/Field.test.tsx | 2 +- .../components/FormInputs/tests/UID.test.tsx | 2 +- .../pages/EditView/components/Header.tsx | 2 +- .../EditView/components/InputRenderer.tsx | 4 +- .../ListConfigurationPage.tsx | 4 +- .../components/DraggableCard.tsx | 2 +- .../components/EditFieldForm.tsx | 4 +- .../ListConfiguration/components/Header.tsx | 2 +- .../ListConfiguration/components/Settings.tsx | 4 +- .../components/SortDisplayedFields.tsx | 2 +- packages/core/admin/admin/src/index.ts | 6 + .../Auth/components/FieldActionWrapper.tsx | 14 - .../pages/Auth/components/ForgotPassword.tsx | 78 +-- .../admin/src/pages/Auth/components/Login.tsx | 158 +++--- .../src/pages/Auth/components/Register.tsx | 449 +++++++++--------- .../pages/Auth/components/ResetPassword.tsx | 209 +++----- .../components/tests/ForgotPassword.test.tsx | 2 +- .../Auth/components/tests/Register.test.tsx | 2 +- .../components/tests/ResetPassword.test.tsx | 7 +- .../admin/admin/src/pages/ProfilePage.tsx | 3 +- .../pages/ApiTokens/EditView/EditViewPage.tsx | 3 +- .../pages/Settings/pages/Roles/CreatePage.tsx | 3 +- .../pages/TransferTokens/EditView.tsx | 3 +- .../pages/Settings/pages/Users/EditPage.tsx | 9 +- .../pages/Users/components/NewUserForm.tsx | 3 +- .../pages/Webhooks/components/WebhookForm.tsx | 3 +- .../core/admin/admin/src/translations/en.json | 2 +- .../src/{content-manager => }/utils/object.ts | 0 .../src/{content-manager => }/utils/refs.ts | 0 .../utils/tests/object.test.ts | 0 .../utils/tests/refs.test.ts | 0 .../EditView/components/AssigneeSelect.tsx | 2 +- .../pages/EditView/components/StageSelect.tsx | 2 +- .../components/tests/AssigneeSelect.test.tsx | 2 +- .../components/tests/StageSelect.test.tsx | 2 +- .../ReviewWorkflows/components/Stage.tsx | 2 +- .../SettingsPage/pages/SingleSignOnPage.tsx | 3 +- .../tests/CMReleasesContainer.test.tsx | 2 +- .../core/email/admin/src/pages/Settings.tsx | 2 +- .../core/email/admin/src/translations/en.json | 2 +- .../core/helper-plugin/MIGRATION_GUIDE.md | 14 + .../helper-plugin/src/components/Form.tsx | 43 -- .../src/components/NotAllowedInput.tsx | 3 +- packages/core/helper-plugin/src/index.ts | 1 - .../src/templates/PageTemplate.stories.mdx | 82 ---- .../BulkMoveDialog/BulkMoveDialog.jsx | 4 +- .../src/components/EditAssetDialog/index.jsx | 4 +- .../EditFolderDialog/EditFolderDialog.jsx | 4 +- .../AddAssetStep/FromUrlForm.jsx | 4 +- .../admin/src/pages/SettingsPage/index.jsx | 3 +- .../admin/src/components/CreateLocale.tsx | 1 + .../admin/src/components/FormModal/index.jsx | 3 +- .../src/pages/AdvancedSettings/index.jsx | 3 +- .../EmailTemplates/components/EmailForm.jsx | 4 +- .../src/pages/Roles/pages/CreatePage.jsx | 3 +- .../admin/src/pages/Roles/pages/EditPage.jsx | 7 +- .../admin/src/translations/en.json | 2 +- 101 files changed, 607 insertions(+), 774 deletions(-) rename packages/core/admin/admin/src/{content-manager => }/components/Form.tsx (95%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Boolean.tsx (93%) create mode 100644 packages/core/admin/admin/src/components/FormInputs/Checkbox.tsx rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Date.tsx (91%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/DateTime.tsx (91%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Email.tsx (89%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Enumeration.tsx (91%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Json.tsx (91%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Number.tsx (90%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Password.tsx (94%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Renderer.tsx (92%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/String.tsx (89%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Textarea.tsx (89%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/Time.tsx (91%) rename packages/core/admin/admin/src/{content-manager => }/components/FormInputs/types.ts (72%) delete mode 100644 packages/core/admin/admin/src/pages/Auth/components/FieldActionWrapper.tsx rename packages/core/admin/admin/src/{content-manager => }/utils/object.ts (100%) rename packages/core/admin/admin/src/{content-manager => }/utils/refs.ts (100%) rename packages/core/admin/admin/src/{content-manager => }/utils/tests/object.test.ts (100%) rename packages/core/admin/admin/src/{content-manager => }/utils/tests/refs.test.ts (100%) delete mode 100644 packages/core/helper-plugin/src/components/Form.tsx delete mode 100644 packages/core/helper-plugin/src/templates/PageTemplate.stories.mdx diff --git a/e2e/tests/admin/signup.spec.ts b/e2e/tests/admin/signup.spec.ts index a603fb7a3c..2b41e93274 100644 --- a/e2e/tests/admin/signup.spec.ts +++ b/e2e/tests/admin/signup.spec.ts @@ -60,7 +60,7 @@ test.describe('Sign Up', () => { await expect(page.getByText('The value must be a lowercase string')).toBeVisible(); await fillEmailAndSubmit('notanemail'); - await expect(page.getByText('This is an invalid email')).toBeVisible(); + await expect(page.getByText('This is not a valid email')).toBeVisible(); }); test("a user cannot submit the form if a password isn't provided or doesn't meet the password validation requirements", async ({ diff --git a/e2e/utils/login.ts b/e2e/utils/login.ts index ea2cae210b..23254771b3 100644 --- a/e2e/utils/login.ts +++ b/e2e/utils/login.ts @@ -13,7 +13,7 @@ export const login = async ({ page, rememberMe = false }: { page: Page; remember .fill(ADMIN_PASSWORD); if (rememberMe) { - await page.getByLabel('rememberMe').click(); + await page.getByLabel('Remember me').click(); } await page.getByRole('button', { name: 'Login' }).click(); diff --git a/packages/core/admin/admin/src/content-manager/components/Form.tsx b/packages/core/admin/admin/src/components/Form.tsx similarity index 95% rename from packages/core/admin/admin/src/content-manager/components/Form.tsx rename to packages/core/admin/admin/src/components/Form.tsx index 7264709175..748672547e 100644 --- a/packages/core/admin/admin/src/content-manager/components/Form.tsx +++ b/packages/core/admin/admin/src/components/Form.tsx @@ -17,8 +17,10 @@ import isEqual from 'lodash/isEqual'; import { useIntl } from 'react-intl'; import { useBlocker } from 'react-router-dom'; -import { createContext } from '../../components/Context'; import { getIn, setIn } from '../utils/object'; +import { useComposedRefs } from '../utils/refs'; + +import { createContext } from './Context'; import type { InputProps as InputPropsImpl, EnumerationProps } from './FormInputs/types'; import type * as Yup from 'yup'; @@ -118,6 +120,7 @@ interface FormProps */ const Form = React.forwardRef( ({ disabled = false, method, onSubmit, ...props }, ref) => { + const formRef = React.useRef(null!); const initialValues = React.useRef(props.initialValues ?? {}); const [state, dispatch] = React.useReducer(reducer, { errors: {}, @@ -146,6 +149,31 @@ const Form = React.forwardRef( }); }, []); + React.useEffect(() => { + if (Object.keys(state.errors).length === 0) return; + + /** + * Small timeout to ensure the form has been + * rendered before we try to focus on the first + */ + const ref = setTimeout(() => { + const [firstError] = formRef.current.querySelectorAll('[data-strapi-field-error]'); + + if (firstError) { + const errorId = firstError.getAttribute('id'); + const formElementInError = formRef.current.querySelector( + `[aria-describedby="${errorId}"]` + ); + + if (formElementInError && formElementInError instanceof HTMLElement) { + formElementInError.focus(); + } + } + }); + + return () => clearTimeout(ref); + }, [state.errors]); + /** * Uses the provided validation schema */ @@ -344,8 +372,10 @@ const Form = React.forwardRef( dispatch({ type: 'SET_ISSUBMITTING', payload: isSubmitting }); }, []); + const composedRefs = useComposedRefs(formRef, ref); + return ( -
+ ( checked={field.value === null ? null : field.value || false} disabled={disabled} hint={hint} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} error={field.error} /** diff --git a/packages/core/admin/admin/src/components/FormInputs/Checkbox.tsx b/packages/core/admin/admin/src/components/FormInputs/Checkbox.tsx new file mode 100644 index 0000000000..7e8956bc08 --- /dev/null +++ b/packages/core/admin/admin/src/components/FormInputs/Checkbox.tsx @@ -0,0 +1,34 @@ +import { forwardRef } from 'react'; + +import { Checkbox } from '@strapi/design-system'; +import { useFocusInputField } from '@strapi/helper-plugin'; + +import { useComposedRefs } from '../../utils/refs'; +import { useField } from '../Form'; + +import { InputProps } from './types'; + +const CheckboxInput = forwardRef( + ({ disabled, label, hint, name, required }, ref) => { + const field = useField(name); + const fieldRef = useFocusInputField(name); + + const composedRefs = useComposedRefs(ref, fieldRef); + + return ( + field.onChange(name, checked)} + ref={composedRefs} + required={required} + value={field.value} + > + {label} + + ); + } +); + +export { CheckboxInput }; diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Date.tsx b/packages/core/admin/admin/src/components/FormInputs/Date.tsx similarity index 91% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Date.tsx rename to packages/core/admin/admin/src/components/FormInputs/Date.tsx index 728b089763..c17f659344 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Date.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Date.tsx @@ -23,6 +23,7 @@ const DateInput = forwardRef( clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })} disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/DateTime.tsx b/packages/core/admin/admin/src/components/FormInputs/DateTime.tsx similarity index 91% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/DateTime.tsx rename to packages/core/admin/admin/src/components/FormInputs/DateTime.tsx index 013165fc93..d6862be455 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/DateTime.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/DateTime.tsx @@ -23,6 +23,7 @@ const DateTimeInput = forwardRef( clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })} disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Email.tsx b/packages/core/admin/admin/src/components/FormInputs/Email.tsx similarity index 89% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Email.tsx rename to packages/core/admin/admin/src/components/FormInputs/Email.tsx index 1125cdbdf6..53949226ab 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Email.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Email.tsx @@ -21,6 +21,7 @@ export const EmailInput = forwardRef( autoComplete="email" disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Enumeration.tsx b/packages/core/admin/admin/src/components/FormInputs/Enumeration.tsx similarity index 91% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Enumeration.tsx rename to packages/core/admin/admin/src/components/FormInputs/Enumeration.tsx index 6a43a33f8b..ba872076fc 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Enumeration.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Enumeration.tsx @@ -20,6 +20,7 @@ export const EnumerationInput = forwardRef( ref={composedRefs} disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} hint={hint} name={name} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Json.tsx b/packages/core/admin/admin/src/components/FormInputs/Json.tsx similarity index 91% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Json.tsx rename to packages/core/admin/admin/src/components/FormInputs/Json.tsx index 4536ba1bbf..60a3b4737f 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Json.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Json.tsx @@ -21,6 +21,7 @@ export const JsonInput = forwardRef( return ( ( defaultValue={field.initialValue} disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Password.tsx b/packages/core/admin/admin/src/components/FormInputs/Password.tsx similarity index 94% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Password.tsx rename to packages/core/admin/admin/src/components/FormInputs/Password.tsx index 0460124b14..44eb16aada 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Password.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Password.tsx @@ -48,6 +48,7 @@ export const PasswordInput = forwardRef( )} } + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Renderer.tsx b/packages/core/admin/admin/src/components/FormInputs/Renderer.tsx similarity index 92% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Renderer.tsx rename to packages/core/admin/admin/src/components/FormInputs/Renderer.tsx index d3e400a9da..0db540198a 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Renderer.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Renderer.tsx @@ -7,6 +7,7 @@ import { useComposedRefs } from '../../utils/refs'; import { useField } from '../Form'; import { BooleanInput } from './Boolean'; +import { CheckboxInput } from './Checkbox'; import { DateInput } from './Date'; import { DateTimeInput } from './DateTime'; import { EmailInput } from './Email'; @@ -38,6 +39,8 @@ const InputRenderer = memo( return ; case 'boolean': return ; + case 'checkbox': + return ; case 'datetime': return ; case 'date': @@ -76,6 +79,7 @@ const NotSupportedField = forwardRef((props, ref) => { ref={composedRefs} disabled error={error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={props.label} id={props.name} hint={props.hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/String.tsx b/packages/core/admin/admin/src/components/FormInputs/String.tsx similarity index 89% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/String.tsx rename to packages/core/admin/admin/src/components/FormInputs/String.tsx index 82cfc4a9ba..301f6688fa 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/String.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/String.tsx @@ -23,6 +23,7 @@ export const StringInput = forwardRef( ref={composedRefs} disabled={disabled} hint={hint} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} name={name} error={field.error} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Textarea.tsx b/packages/core/admin/admin/src/components/FormInputs/Textarea.tsx similarity index 89% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Textarea.tsx rename to packages/core/admin/admin/src/components/FormInputs/Textarea.tsx index e57d4561b5..a727d6daec 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Textarea.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Textarea.tsx @@ -21,6 +21,7 @@ export const TextareaInput = forwardRef( disabled={disabled} defaultValue={field.initialValue} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/Time.tsx b/packages/core/admin/admin/src/components/FormInputs/Time.tsx similarity index 91% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/Time.tsx rename to packages/core/admin/admin/src/components/FormInputs/Time.tsx index 9dfc2c152f..07138674e5 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/Time.tsx +++ b/packages/core/admin/admin/src/components/FormInputs/Time.tsx @@ -23,6 +23,7 @@ const TimeInput = forwardRef( clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })} disabled={disabled} error={field.error} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} id={name} hint={hint} diff --git a/packages/core/admin/admin/src/content-manager/components/FormInputs/types.ts b/packages/core/admin/admin/src/components/FormInputs/types.ts similarity index 72% rename from packages/core/admin/admin/src/content-manager/components/FormInputs/types.ts rename to packages/core/admin/admin/src/components/FormInputs/types.ts index f13ea04928..c186f79995 100644 --- a/packages/core/admin/admin/src/content-manager/components/FormInputs/types.ts +++ b/packages/core/admin/admin/src/components/FormInputs/types.ts @@ -15,22 +15,24 @@ interface EnumerationProps extends Omit { interface InputProps { disabled?: boolean; hint?: ReactNode; - label: string; + label: ReactNode; name: string; placeholder?: string; required?: boolean; options?: never; - type: Exclude< - Attribute.Kind, - | 'enumeration' - | 'media' - | 'blocks' - | 'richtext' - | 'uid' - | 'dynamiczone' - | 'component' - | 'relation' - >; + type: + | Exclude< + Attribute.Kind, + | 'enumeration' + | 'media' + | 'blocks' + | 'richtext' + | 'uid' + | 'dynamiczone' + | 'component' + | 'relation' + > + | 'checkbox'; } export { EnumerationProps, InputProps }; diff --git a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/EditFieldForm.tsx b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/EditFieldForm.tsx index 2f9bafc373..4837eec91b 100644 --- a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/EditFieldForm.tsx +++ b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/EditFieldForm.tsx @@ -15,13 +15,13 @@ import { useNotification } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; import * as yup from 'yup'; +import { Form, InputProps, useField } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { capitalise } from '../../../utils/strings'; import { ATTRIBUTE_TYPES_THAT_CANNOT_BE_MAIN_FIELD } from '../../constants/attributes'; import { useGetInitialDataQuery } from '../../services/init'; import { getTranslation } from '../../utils/translations'; import { FieldTypeIcon } from '../FieldTypeIcon'; -import { Form, InputProps, useField } from '../Form'; -import { InputRenderer } from '../FormInputs/Renderer'; import { TEMP_FIELD_NAME } from './Fields'; diff --git a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Fields.tsx b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Fields.tsx index ff5cced2db..93f75efd62 100644 --- a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Fields.tsx +++ b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Fields.tsx @@ -9,12 +9,12 @@ import { useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; +import { useField, useForm } from '../../../components/Form'; +import { useComposedRefs } from '../../../utils/refs'; import { ItemTypes } from '../../constants/dragAndDrop'; import { type UseDragAndDropOptions, useDragAndDrop } from '../../hooks/useDragAndDrop'; -import { useComposedRefs } from '../../utils/refs'; import { getTranslation } from '../../utils/translations'; import { ComponentIcon } from '../ComponentIcon'; -import { useField, useForm } from '../Form'; import { EditFieldForm, EditFieldFormProps } from './EditFieldForm'; @@ -226,7 +226,7 @@ const Fields = ({ attributes, fieldSizes, components, metadatas = {} }: FieldsPr {remainingFields.map((field) => ( - + {field.label} ))} diff --git a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Form.tsx b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Form.tsx index e6a12c367f..c1bb38bcf6 100644 --- a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Form.tsx +++ b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/Form.tsx @@ -19,11 +19,11 @@ import pipe from 'lodash/fp/pipe'; import { useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; +import { Form, FormProps, useForm } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { capitalise } from '../../../utils/strings'; import { ATTRIBUTE_TYPES_THAT_CANNOT_BE_MAIN_FIELD } from '../../constants/attributes'; import { getTranslation } from '../../utils/translations'; -import { Form, FormProps, useForm } from '../Form'; -import { InputRenderer } from '../FormInputs/Renderer'; import { Fields, FieldsProps, TEMP_FIELD_NAME } from './Fields'; diff --git a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/tests/EditFieldForm.test.tsx b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/tests/EditFieldForm.test.tsx index 852f4bced2..f7ee41ef2c 100644 --- a/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/tests/EditFieldForm.test.tsx +++ b/packages/core/admin/admin/src/content-manager/components/ConfigurationForm/tests/EditFieldForm.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render as renderRTL, screen } from '@tests/utils'; -import { Form } from '../../Form'; +import { Form } from '../../../../components/Form'; import { EditFieldForm, EditFieldFormProps } from '../EditFieldForm'; import { ConfigurationFormData } from '../Form'; diff --git a/packages/core/admin/admin/src/content-manager/components/Relations/RelationInput.tsx b/packages/core/admin/admin/src/content-manager/components/Relations/RelationInput.tsx index 0e79523d11..2994de0265 100644 --- a/packages/core/admin/admin/src/content-manager/components/Relations/RelationInput.tsx +++ b/packages/core/admin/admin/src/content-manager/components/Relations/RelationInput.tsx @@ -23,6 +23,7 @@ import { useIntl } from 'react-intl'; import { FixedSizeList, FixedSizeList as List, ListChildComponentProps } from 'react-window'; import styled from 'styled-components'; +import { composeRefs } from '../../../utils/refs'; import { ItemTypes } from '../../constants/dragAndDrop'; import { UseDragAndDropOptions, @@ -30,7 +31,6 @@ import { DROP_SENSITIVITY, } from '../../hooks/useDragAndDrop'; import { usePrev } from '../../hooks/usePrev'; -import { composeRefs } from '../../utils/refs'; import { getTranslation } from '../../utils/translations'; import type { NormalizedRelation } from './utils/normalizeRelations'; diff --git a/packages/core/admin/admin/src/content-manager/exports.ts b/packages/core/admin/admin/src/content-manager/exports.ts index 71b0d37252..52c37eeaef 100644 --- a/packages/core/admin/admin/src/content-manager/exports.ts +++ b/packages/core/admin/admin/src/content-manager/exports.ts @@ -14,5 +14,3 @@ export type { ListLayout, } from './hooks/useDocumentLayout'; export * from './features/DocumentRBAC'; -export * from './components/Form'; -export * from './components/FormInputs/Renderer'; diff --git a/packages/core/admin/admin/src/content-manager/hooks/useDocumentLayout.ts b/packages/core/admin/admin/src/content-manager/hooks/useDocumentLayout.ts index 458dca2553..d243c3fe34 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/useDocumentLayout.ts +++ b/packages/core/admin/admin/src/content-manager/hooks/useDocumentLayout.ts @@ -20,7 +20,7 @@ import { useDocument, } from './useDocument'; -import type { InputProps } from '../components/FormInputs/types'; +import type { InputProps } from '../../components/FormInputs/types'; import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared'; import type { Attribute } from '@strapi/types'; import type { MessageDescriptor } from 'react-intl'; @@ -66,8 +66,9 @@ interface ListLayout { options: LayoutOptions; settings: LayoutSettings; } -interface EditFieldSharedProps extends Omit { +interface EditFieldSharedProps extends Omit { hint?: string; + label: string; mainField?: string; size: number; unique?: boolean; diff --git a/packages/core/admin/admin/src/content-manager/pages/ComponentConfigurationPage.tsx b/packages/core/admin/admin/src/content-manager/pages/ComponentConfigurationPage.tsx index 5df7f3ccb5..57a8776a1b 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ComponentConfigurationPage.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ComponentConfigurationPage.tsx @@ -10,6 +10,7 @@ import { import { useParams } from 'react-router-dom'; import { useTypedSelector } from '../../core/store/hooks'; +import { setIn } from '../../utils/object'; import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields'; import { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form'; import { ComponentsDictionary, extractContentTypeComponents } from '../hooks/useDocument'; @@ -23,7 +24,6 @@ import { useUpdateComponentConfigurationMutation, } from '../services/components'; import { useGetInitialDataQuery } from '../services/init'; -import { setIn } from '../utils/object'; import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditConfigurationPage.tsx b/packages/core/admin/admin/src/content-manager/pages/EditConfigurationPage.tsx index d12b202802..9f5f8453ab 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditConfigurationPage.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditConfigurationPage.tsx @@ -10,13 +10,13 @@ import { } from '@strapi/helper-plugin'; import { useTypedSelector } from '../../core/store/hooks'; +import { setIn } from '../../utils/object'; import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields'; import { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form'; import { useDoc } from '../hooks/useDocument'; import { useDocLayout } from '../hooks/useDocumentLayout'; import { useUpdateContentTypeConfigurationMutation } from '../services/contentTypes'; import { useGetInitialDataQuery } from '../services/init'; -import { setIn } from '../utils/object'; import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/EditViewPage.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/EditViewPage.tsx index 2f7c66228b..24415b8900 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/EditViewPage.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/EditViewPage.tsx @@ -23,8 +23,8 @@ import { useIntl } from 'react-intl'; import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; +import { Blocker, Form } from '../../../components/Form'; import { useOnce } from '../../../hooks/useOnce'; -import { Blocker, Form } from '../../components/Form'; import { SINGLE_TYPES } from '../../constants/collections'; import { DocumentRBAC, useDocumentRBAC } from '../../features/DocumentRBAC'; import { type UseDocument, useDoc } from '../../hooks/useDocument'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/DocumentActions.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/DocumentActions.tsx index aba4e05f2f..50f8e9d39f 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/DocumentActions.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/DocumentActions.tsx @@ -23,9 +23,9 @@ import { useIntl } from 'react-intl'; import { useMatch, useNavigate } from 'react-router-dom'; import styled, { DefaultTheme } from 'styled-components'; +import { useForm } from '../../../../components/Form'; import { DocumentActionComponent } from '../../../../core/apis/content-manager'; import { isBaseQueryError } from '../../../../utils/baseQuery'; -import { useForm } from '../../../components/Form'; import { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes'; import { SINGLE_TYPES } from '../../../constants/collections'; import { useDocumentRBAC } from '../../../features/DocumentRBAC'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx index 3ce8192bb6..4a9dec7c6d 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx @@ -14,7 +14,7 @@ import { Editor, Path, Range, Transforms } from 'slate'; import { type RenderElementProps, ReactEditor } from 'slate-react'; import styled from 'styled-components'; -import { composeRefs } from '../../../../../../utils/refs'; +import { composeRefs } from '../../../../../../../utils/refs'; import { type BlocksStore, useBlocksEditorContext } from '../BlocksEditor'; import { editLink, removeLink } from '../utils/links'; import { isLinkNode, type Block } from '../utils/types'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx index 31208dea8a..4fa9ebb844 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx @@ -7,9 +7,9 @@ import { Editor, Range, Transforms } from 'slate'; import { ReactEditor, type RenderElementProps, type RenderLeafProps, Editable } from 'slate-react'; import styled, { CSSProperties, css } from 'styled-components'; +import { composeRefs } from '../../../../../../utils/refs'; import { ItemTypes } from '../../../../../constants/dragAndDrop'; import { useDragAndDrop, DIRECTIONS } from '../../../../../hooks/useDragAndDrop'; -import { composeRefs } from '../../../../../utils/refs'; import { getTranslation } from '../../../../../utils/translations'; import { type BlocksStore, useBlocksEditorContext } from './BlocksEditor'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx index 1d926adb45..fdb1c04225 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx @@ -11,7 +11,7 @@ import { withHistory } from 'slate-history'; import { type RenderElementProps, Slate, withReact, ReactEditor, useSlate } from 'slate-react'; import styled, { type CSSProperties } from 'styled-components'; -import { FieldValue } from '../../../../../components/Form'; +import { FieldValue } from '../../../../../../components/Form'; import { getTranslation } from '../../../../../utils/translations'; import { codeBlocks } from './Blocks/Code'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx index 7833207a45..e96cf3fd43 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { Field, FieldError, FieldHint, FieldLabel, Flex } from '@strapi/design-system'; -import { useField } from '../../../../../components/Form'; -import { InputProps } from '../../../../../components/FormInputs/types'; +import { useField } from '../../../../../../components/Form'; +import { InputProps } from '../../../../../../components/FormInputs/types'; import { BlocksEditor } from './BlocksEditor'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/tests/BlocksInput.test.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/tests/BlocksInput.test.tsx index c3bea7cdcf..dc7a3049ae 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/tests/BlocksInput.test.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/BlocksInput/tests/BlocksInput.test.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { render, screen, waitFor } from '@tests/utils'; -import { Form } from '../../../../../../components/Form'; +import { Form } from '../../../../../../../components/Form'; import { BlocksInput } from '../BlocksInput'; import { blocksData } from './mock-schema'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Initializer.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Initializer.tsx index 691736eae2..15ec5123d1 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Initializer.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Initializer.tsx @@ -6,7 +6,7 @@ import { PlusCircle } from '@strapi/icons'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; -import { useField } from '../../../../../components/Form'; +import { useField } from '../../../../../../components/Form'; import { getTranslation } from '../../../../../utils/translations'; interface InitializerProps { diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Input.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Input.tsx index 6e23f0798a..a7e73a116c 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Input.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Input.tsx @@ -2,7 +2,7 @@ import { Box, Flex, IconButton, Typography } from '@strapi/design-system'; import { Trash } from '@strapi/icons'; import { useIntl } from 'react-intl'; -import { InputProps, useField } from '../../../../../components/Form'; +import { InputProps, useField } from '../../../../../../components/Form'; import { useDoc } from '../../../../../hooks/useDocument'; import { EditFieldLayout } from '../../../../../hooks/useDocumentLayout'; import { getTranslation } from '../../../../../utils/translations'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Repeatable.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Repeatable.tsx index d298a1952f..67e69e28a2 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Repeatable.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Component/Repeatable.tsx @@ -20,13 +20,13 @@ import { getEmptyImage } from 'react-dnd-html5-backend'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; -import { useField, useForm } from '../../../../../components/Form'; +import { useField, useForm } from '../../../../../../components/Form'; +import { getIn } from '../../../../../../utils/object'; +import { useComposedRefs } from '../../../../../../utils/refs'; import { ItemTypes } from '../../../../../constants/dragAndDrop'; import { useDoc } from '../../../../../hooks/useDocument'; import { useDocLayout } from '../../../../../hooks/useDocumentLayout'; import { useDragAndDrop, type UseDragAndDropOptions } from '../../../../../hooks/useDragAndDrop'; -import { getIn } from '../../../../../utils/object'; -import { useComposedRefs } from '../../../../../utils/refs'; import { getTranslation } from '../../../../../utils/translations'; import { transformDocument } from '../../../utils/data'; import { createDefaultForm } from '../../../utils/forms'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.tsx index 4ceaf4d06e..f70b48b014 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.tsx @@ -17,13 +17,13 @@ import { getEmptyImage } from 'react-dnd-html5-backend'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; +import { useForm } from '../../../../../../components/Form'; +import { getIn } from '../../../../../../utils/object'; +import { useComposedRefs } from '../../../../../../utils/refs'; import { ComponentIcon } from '../../../../../components/ComponentIcon'; -import { useForm } from '../../../../../components/Form'; import { ItemTypes } from '../../../../../constants/dragAndDrop'; import { useDocLayout } from '../../../../../hooks/useDocumentLayout'; import { type UseDragAndDropOptions, useDragAndDrop } from '../../../../../hooks/useDragAndDrop'; -import { getIn } from '../../../../../utils/object'; -import { useComposedRefs } from '../../../../../utils/refs'; import { getTranslation } from '../../../../../utils/translations'; import { InputRenderer } from '../../InputRenderer'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicZoneLabel.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicZoneLabel.tsx index 60bf53df70..92df1759f9 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicZoneLabel.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/DynamicZoneLabel.tsx @@ -4,7 +4,7 @@ import { Box, Flex, Typography } from '@strapi/design-system'; import { pxToRem } from '@strapi/helper-plugin'; interface DynamicZoneLabelProps { - label?: string; + label?: React.ReactNode; labelAction?: React.ReactNode; name: string; numberOfComponents?: number; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/Field.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/Field.tsx index dde7bb8b9f..49c3856b79 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/Field.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/Field.tsx @@ -6,7 +6,7 @@ import pipe from 'lodash/fp/pipe'; import { useIntl } from 'react-intl'; import { createContext } from '../../../../../../components/Context'; -import { InputProps, useField, useForm } from '../../../../../components/Form'; +import { InputProps, useField, useForm } from '../../../../../../components/Form'; import { useDoc } from '../../../../../hooks/useDocument'; import { type EditFieldLayout } from '../../../../../hooks/useDocumentLayout'; import { getTranslation } from '../../../../../utils/translations'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/DynamicComponent.test.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/DynamicComponent.test.tsx index 46dcdcb12e..01e80966a4 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/DynamicComponent.test.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/DynamicComponent.test.tsx @@ -1,7 +1,7 @@ import { screen, fireEvent, render as renderRTL } from '@tests/utils'; import { Route, Routes } from 'react-router-dom'; -import { Form } from '../../../../../../components/Form'; +import { Form } from '../../../../../../../components/Form'; import { DynamicComponent, DynamicComponentProps } from '../DynamicComponent'; import { dynamicComponentsByCategory } from './fixtures'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/Field.test.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/Field.test.tsx index 7c7b255f17..14457d9ce4 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/Field.test.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/DynamicZone/tests/Field.test.tsx @@ -1,7 +1,7 @@ import { act, render as renderRTL, screen } from '@tests/utils'; import { Route, Routes } from 'react-router-dom'; -import { Form } from '../../../../../../components/Form'; +import { Form } from '../../../../../../../components/Form'; import { DynamicZone, DynamicZoneProps } from '../Field'; const TEST_NAME = 'DynamicZoneComponent'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/UID.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/UID.tsx index b60b7e8f17..3feedb5dd2 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/UID.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/UID.tsx @@ -12,8 +12,9 @@ import { Contracts } from '@strapi/plugin-content-manager/_internal/shared'; import { useIntl } from 'react-intl'; import styled, { keyframes } from 'styled-components'; +import { type InputProps, useField, useForm } from '../../../../../components/Form'; import { useDebounce } from '../../../../../hooks/useDebounce'; -import { type InputProps, useField, useForm } from '../../../../components/Form'; +import { useComposedRefs } from '../../../../../utils/refs'; import { useDoc } from '../../../../hooks/useDocument'; import { useGenerateUIDMutation, @@ -21,7 +22,6 @@ import { useGetDefaultUIDQuery, } from '../../../../services/uid'; import { buildValidParams } from '../../../../utils/api'; -import { useComposedRefs } from '../../../../utils/refs'; import type { Attribute } from '@strapi/types'; @@ -246,6 +246,7 @@ const UIDInput = React.forwardRef( } hint={hint} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} name={name} onChange={field.onChange} diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Editor.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Editor.tsx index 2177e01305..2de2687fdd 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Editor.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Editor.tsx @@ -6,8 +6,8 @@ import styled from 'styled-components'; import { PreviewWysiwyg } from './PreviewWysiwyg'; import { newlineAndIndentContinueMarkdownList } from './utils/continueList'; -import type { FieldValue } from '../../../../../components/Form'; -import type { InputProps } from '../../../../../components/FormInputs/types'; +import type { FieldValue } from '../../../../../../components/Form'; +import type { InputProps } from '../../../../../../components/FormInputs/types'; import 'codemirror5/addon/display/placeholder'; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Field.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Field.tsx index 8464ab7a1f..760987308c 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Field.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/Field.tsx @@ -4,7 +4,7 @@ import { Field, FieldError, FieldHint, FieldLabel, Flex } from '@strapi/design-s import { prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin'; import { EditorFromTextArea } from 'codemirror5'; -import { useField } from '../../../../../components/Form'; +import { useField } from '../../../../../../components/Form'; import { Editor, EditorApi } from './Editor'; import { EditorLayout } from './EditorLayout'; @@ -18,7 +18,7 @@ import { import { WysiwygFooter } from './WysiwygFooter'; import { WysiwygNav } from './WysiwygNav'; -import type { InputProps } from '../../../../../components/FormInputs/types'; +import type { InputProps } from '../../../../../../components/FormInputs/types'; import type { Attribute } from '@strapi/types'; interface WysiwygProps extends Omit { diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/tests/Field.test.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/tests/Field.test.tsx index 375314dcae..798e02df15 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/tests/Field.test.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/Wysiwyg/tests/Field.test.tsx @@ -1,6 +1,6 @@ import { render as renderRTL } from '@tests/utils'; -import { Form } from '../../../../../../components/Form'; +import { Form } from '../../../../../../../components/Form'; import { Wysiwyg, WysiwygProps } from '../Field'; jest.mock('@strapi/helper-plugin', () => ({ diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/tests/UID.test.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/tests/UID.test.tsx index 5746cfe149..d1699cbf79 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/tests/UID.test.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/FormInputs/tests/UID.test.tsx @@ -1,7 +1,7 @@ import { render as renderRTL, waitFor, act, screen } from '@tests/utils'; import { Route, Routes } from 'react-router-dom'; -import { Form } from '../../../../../components/Form'; +import { Form } from '../../../../../../components/Form'; import { UIDInput, UIDInputProps } from '../UID'; const render = ({ diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/Header.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/Header.tsx index 6e874e4363..74338673a8 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/Header.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/Header.tsx @@ -16,8 +16,8 @@ import { useMatch, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { DescriptionComponentRenderer } from '../../../../components/DescriptionComponentRenderer'; +import { useForm } from '../../../../components/Form'; import { capitalise } from '../../../../utils/strings'; -import { useForm } from '../../../components/Form'; import { CREATED_AT_ATTRIBUTE_NAME, CREATED_BY_ATTRIBUTE_NAME, diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/components/InputRenderer.tsx b/packages/core/admin/admin/src/content-manager/pages/EditView/components/InputRenderer.tsx index aea8ce4734..0de4667c35 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/components/InputRenderer.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/components/InputRenderer.tsx @@ -3,8 +3,8 @@ import { ReactNode } from 'react'; import { NotAllowedInput, useLibrary } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; -import { useForm } from '../../../components/Form'; -import { InputRenderer as FormInputRenderer } from '../../../components/FormInputs/Renderer'; +import { useForm } from '../../../../components/Form'; +import { InputRenderer as FormInputRenderer } from '../../../../components/FormInputs/Renderer'; import { useDocumentRBAC } from '../../../features/DocumentRBAC'; import { useDoc } from '../../../hooks/useDocument'; import { useLazyComponents } from '../../../hooks/useLazyComponents'; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/ListConfigurationPage.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/ListConfigurationPage.tsx index f183780fe9..de2839b06b 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/ListConfigurationPage.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/ListConfigurationPage.tsx @@ -11,13 +11,13 @@ import { import { useIntl } from 'react-intl'; import { Navigate } from 'react-router-dom'; +import { Form, FormProps } from '../../../components/Form'; import { useTypedSelector } from '../../../core/store/hooks'; -import { Form, FormProps } from '../../components/Form'; +import { setIn } from '../../../utils/object'; import { SINGLE_TYPES } from '../../constants/collections'; import { useDoc } from '../../hooks/useDocument'; import { ListFieldLayout, ListLayout, useDocLayout } from '../../hooks/useDocumentLayout'; import { useUpdateContentTypeConfigurationMutation } from '../../services/contentTypes'; -import { setIn } from '../../utils/object'; import { Header } from './components/Header'; import { Settings } from './components/Settings'; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/DraggableCard.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/DraggableCard.tsx index 3db564baa0..ed0998e41d 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/DraggableCard.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/DraggableCard.tsx @@ -6,10 +6,10 @@ import { getEmptyImage } from 'react-dnd-html5-backend'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; +import { useComposedRefs } from '../../../../utils/refs'; import { CardDragPreview } from '../../../components/DragPreviews/CardDragPreview'; import { ItemTypes } from '../../../constants/dragAndDrop'; import { useDragAndDrop } from '../../../hooks/useDragAndDrop'; -import { useComposedRefs } from '../../../utils/refs'; import { getTranslation } from '../../../utils/translations'; import { EditFieldForm } from './EditFieldForm'; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/EditFieldForm.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/EditFieldForm.tsx index b37b4c0674..0cde284d3c 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/EditFieldForm.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/EditFieldForm.tsx @@ -16,10 +16,10 @@ import { useIntl } from 'react-intl'; import styled from 'styled-components'; import * as yup from 'yup'; +import { Form, useField } from '../../../../components/Form'; +import { InputRenderer } from '../../../../components/FormInputs/Renderer'; import { capitalise } from '../../../../utils/strings'; import { FieldTypeIcon } from '../../../components/FieldTypeIcon'; -import { Form, useField } from '../../../components/Form'; -import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { getTranslation } from '../../../utils/translations'; import type { ListFieldLayout } from '../../../hooks/useDocumentLayout'; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Header.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Header.tsx index 5a63cd0e30..4c84dbb53c 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Header.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Header.tsx @@ -5,8 +5,8 @@ import { ArrowLeft } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; +import { useForm } from '../../../../components/Form'; import { capitalise } from '../../../../utils/strings'; -import { useForm } from '../../../components/Form'; import { getTranslation } from '../../../utils/translations'; interface HeaderProps { diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Settings.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Settings.tsx index cd007a3ca2..90b6bacb96 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Settings.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/Settings.tsx @@ -4,9 +4,9 @@ import { Flex, Grid, GridItem, Typography } from '@strapi/design-system'; import { useCollator } from '@strapi/helper-plugin'; import { MessageDescriptor, useIntl } from 'react-intl'; +import { useForm, type InputProps } from '../../../../components/Form'; +import { InputRenderer } from '../../../../components/FormInputs/Renderer'; import { useEnterprise } from '../../../../hooks/useEnterprise'; -import { useForm, type InputProps } from '../../../components/Form'; -import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { useDoc } from '../../../hooks/useDocument'; import { type EditFieldLayout } from '../../../hooks/useDocumentLayout'; import { getTranslation } from '../../../utils/translations'; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/SortDisplayedFields.tsx b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/SortDisplayedFields.tsx index fc54a480e0..fc126e4432 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/SortDisplayedFields.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/ListConfiguration/components/SortDisplayedFields.tsx @@ -5,7 +5,7 @@ import { Menu } from '@strapi/design-system/v2'; import { Plus } from '@strapi/icons'; import { useIntl } from 'react-intl'; -import { useForm } from '../../../components/Form'; +import { useForm } from '../../../../components/Form'; import { useDoc } from '../../../hooks/useDocument'; import { useGetContentTypeConfigurationQuery } from '../../../services/contentTypes'; import { checkIfAttributeIsDisplayable } from '../../../utils/attributes'; diff --git a/packages/core/admin/admin/src/index.ts b/packages/core/admin/admin/src/index.ts index afbb40ee12..c46dbf1e6c 100644 --- a/packages/core/admin/admin/src/index.ts +++ b/packages/core/admin/admin/src/index.ts @@ -4,6 +4,12 @@ */ export * from './render'; +/** + * components + */ +export * from './components/Form'; +export * from './components/FormInputs/Renderer'; + /** * Hooks */ diff --git a/packages/core/admin/admin/src/pages/Auth/components/FieldActionWrapper.tsx b/packages/core/admin/admin/src/pages/Auth/components/FieldActionWrapper.tsx deleted file mode 100644 index a173103486..0000000000 --- a/packages/core/admin/admin/src/pages/Auth/components/FieldActionWrapper.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FieldAction } from '@strapi/design-system'; -import styled from 'styled-components'; - -const FieldActionWrapper = styled(FieldAction)` - svg { - height: 1rem; - width: 1rem; - path { - fill: ${({ theme }) => theme.colors.neutral600}; - } - } -`; - -export { FieldActionWrapper }; diff --git a/packages/core/admin/admin/src/pages/Auth/components/ForgotPassword.tsx b/packages/core/admin/admin/src/pages/Auth/components/ForgotPassword.tsx index c7ba0bbf51..d231940a77 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/ForgotPassword.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/ForgotPassword.tsx @@ -1,11 +1,12 @@ -import { Box, Button, Flex, Main, TextInput, Typography } from '@strapi/design-system'; +import { Box, Button, Flex, Main, Typography } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; -import { Form, translatedErrors, useAPIErrorHandler } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { translatedErrors, useAPIErrorHandler } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; import { NavLink, useNavigate } from 'react-router-dom'; import * as yup from 'yup'; +import { Form } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { Logo } from '../../../components/UnauthenticatedLogo'; import { Column, @@ -49,8 +50,8 @@ const ForgotPassword = () => { ) : null} - { } }} validationSchema={yup.object().shape({ - email: yup.string().email(translatedErrors.email).required(translatedErrors.required), + email: yup + .string() + .email({ + id: translatedErrors.email, + defaultMessage: 'This is not a valid email.', + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'This field is required.', + }), })} - validateOnChange={false} > - {({ values, errors, handleChange }) => ( - - - - - - - )} - + + {[ + { + label: formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' }), + name: 'email', + placeholder: formatMessage({ + id: 'Auth.form.email.placeholder', + defaultMessage: 'kai@doe.com', + }), + required: true, + type: 'string' as const, + }, + ].map((field) => ( + + ))} + + + diff --git a/packages/core/admin/admin/src/pages/Auth/components/Login.tsx b/packages/core/admin/admin/src/pages/Auth/components/Login.tsx index 489cdb9d3e..ed0c76e7b3 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/Login.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/Login.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { Box, Button, Checkbox, Flex, Main, TextInput, Typography } from '@strapi/design-system'; +import { Box, Button, Flex, Main, Typography } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; -import { Form, translatedErrors, useQuery } from '@strapi/helper-plugin'; -import { Eye, EyeStriked } from '@strapi/icons'; -import { Formik } from 'formik'; +import { translatedErrors, useQuery } from '@strapi/helper-plugin'; import camelCase from 'lodash/camelCase'; import { useIntl } from 'react-intl'; import { NavLink, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import * as yup from 'yup'; +import { Form } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { Logo } from '../../../components/UnauthenticatedLogo'; import { useAuth } from '../../../features/Auth'; import { @@ -19,8 +18,6 @@ import { LayoutContent, } from '../../../layouts/UnauthenticatedLayout'; -import { FieldActionWrapper } from './FieldActionWrapper'; - import type { Login } from '../../../../../shared/contracts/authentication'; interface LoginProps { @@ -28,14 +25,25 @@ interface LoginProps { } const LOGIN_SCHEMA = yup.object().shape({ - email: yup.string().email(translatedErrors.email).required(translatedErrors.required), - password: yup.string().required(translatedErrors.required), + email: yup + .string() + .email({ + id: translatedErrors.email, + defaultMessage: 'Not a valid email', + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'This value is required.', + }), + password: yup.string().required({ + id: translatedErrors.required, + defaultMessage: 'This value is required.', + }), rememberMe: yup.bool().nullable(), }); const Login = ({ children }: LoginProps) => { const [apiError, setApiError] = React.useState(); - const [passwordShown, setPasswordShown] = React.useState(false); const { formatMessage } = useIntl(); const query = useQuery(); const navigate = useNavigate(); @@ -92,8 +100,8 @@ const Login = ({ children }: LoginProps) => {
) : null} - { handleLogin(values); }} validationSchema={LOGIN_SCHEMA} - validateOnChange={false} > - {({ values, errors, handleChange }) => ( -
- - - { - e.stopPropagation(); - setPasswordShown((prev) => !prev); - }} - label={formatMessage( - passwordShown - ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } - : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } - )} - > - {passwordShown ? : } - - } - required - /> - { - handleChange({ target: { value: checked, name: 'rememberMe' } }); - }} - value={values.rememberMe} - aria-label="rememberMe" - name="rememberMe" - > - {formatMessage({ - id: 'Auth.form.rememberMe.label', - defaultMessage: 'Remember me', - })} - - - -
- )} -
+ + {[ + { + label: formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' }), + name: 'email', + placeholder: formatMessage({ + id: 'Auth.form.email.placeholder', + defaultMessage: 'kai@doe.com', + }), + required: true, + type: 'string' as const, + }, + { + label: formatMessage({ + id: 'global.password', + defaultMessage: 'Password', + }), + name: 'password', + required: true, + type: 'password' as const, + }, + { + label: formatMessage({ + id: 'Auth.form.rememberMe.label', + defaultMessage: 'Remember me', + }), + name: 'rememberMe', + type: 'checkbox' as const, + }, + ].map((field) => ( + + ))} + + + {children} @@ -205,11 +167,5 @@ const Login = ({ children }: LoginProps) => { ); }; -const PasswordInput = styled(TextInput)` - ::-ms-reveal { - display: none; - } -`; - export { Login }; export type { LoginProps }; diff --git a/packages/core/admin/admin/src/pages/Auth/components/Register.tsx b/packages/core/admin/admin/src/pages/Auth/components/Register.tsx index f7bbe6bc17..6fa2e70c34 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/Register.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/Register.tsx @@ -1,21 +1,9 @@ import * as React from 'react'; -import { - Box, - Button, - Checkbox, - Flex, - Grid, - GridItem, - Main, - TextInput, - Typography, -} from '@strapi/design-system'; +import { Box, Button, Flex, Grid, GridItem, Main, Typography } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; import { - Form, auth, - getYupInnerErrors, translatedErrors, useAPIErrorHandler, useGuidedTour, @@ -23,10 +11,8 @@ import { useQuery, useTracking, } from '@strapi/helper-plugin'; -import { Eye, EyeStriked } from '@strapi/icons'; -import { Formik, FormikHelpers } from 'formik'; import omit from 'lodash/omit'; -import { MessageDescriptor, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import { NavLink, Navigate, useNavigate, useMatch } from 'react-router-dom'; import styled from 'styled-components'; import * as yup from 'yup'; @@ -36,6 +22,8 @@ import { Register as RegisterUser, RegisterAdmin, } from '../../../../../shared/contracts/authentication'; +import { Form, FormHelpers } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { useNpsSurveySettings } from '../../../components/NpsSurvey'; import { Logo } from '../../../components/UnauthenticatedLogo'; import { useAuth } from '../../../features/Auth'; @@ -47,45 +35,117 @@ import { } from '../../../services/auth'; import { isBaseQueryError } from '../../../utils/baseQuery'; -import { FieldActionWrapper } from './FieldActionWrapper'; - const REGISTER_USER_SCHEMA = yup.object().shape({ - firstname: yup.string().trim().required(translatedErrors.required), + firstname: yup.string().trim().required({ + id: translatedErrors.required, + defaultMessage: 'Firstname is required', + }), lastname: yup.string().nullable(), password: yup .string() - .min(8, translatedErrors.minLength) - .matches(/[a-z]/, 'components.Input.error.contain.lowercase') - .matches(/[A-Z]/, 'components.Input.error.contain.uppercase') - .matches(/\d/, 'components.Input.error.contain.number') - .required(translatedErrors.required), + .min(8, { + id: translatedErrors.minLength, + defaultMessage: 'Password must be at least 8 characters', + values: { min: 8 }, + }) + .matches(/[a-z]/, { + message: { + id: 'components.Input.error.contain.lowercase', + defaultMessage: 'Password must contain at least 1 lowercase letter', + }, + }) + .matches(/[A-Z]/, { + message: { + id: 'components.Input.error.contain.uppercase', + defaultMessage: 'Password must contain at least 1 uppercase letter', + }, + }) + .matches(/\d/, { + message: { + id: 'components.Input.error.contain.number', + defaultMessage: 'Password must contain at least 1 number', + }, + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'Password is required', + }), confirmPassword: yup .string() - .oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch') - .required(translatedErrors.required), - registrationToken: yup.string().required(translatedErrors.required), + .required({ + id: translatedErrors.required, + defaultMessage: 'Confirm password is required', + }) + .oneOf([yup.ref('password'), null], { + id: 'components.Input.error.password.noMatch', + defaultMessage: 'Passwords must match', + }), + registrationToken: yup.string().required({ + id: translatedErrors.required, + defaultMessage: 'Registration token is required', + }), }); const REGISTER_ADMIN_SCHEMA = yup.object().shape({ - firstname: yup.string().trim().required(translatedErrors.required), + firstname: yup.string().trim().required({ + id: translatedErrors.required, + defaultMessage: 'Firstname is required', + }), lastname: yup.string().nullable(), password: yup .string() - .min(8, translatedErrors.minLength) - .matches(/[a-z]/, 'components.Input.error.contain.lowercase') - .matches(/[A-Z]/, 'components.Input.error.contain.uppercase') - .matches(/\d/, 'components.Input.error.contain.number') - .required(translatedErrors.required), - email: yup - .string() - .email(translatedErrors.email) - .strict() - .lowercase(translatedErrors.lowercase) - .required(translatedErrors.required), + .min(8, { + id: translatedErrors.minLength, + defaultMessage: 'Password must be at least 8 characters', + values: { min: 8 }, + }) + .matches(/[a-z]/, { + message: { + id: 'components.Input.error.contain.lowercase', + defaultMessage: 'Password must contain at least 1 lowercase letter', + }, + }) + .matches(/[A-Z]/, { + message: { + id: 'components.Input.error.contain.uppercase', + defaultMessage: 'Password must contain at least 1 uppercase letter', + }, + }) + .matches(/\d/, { + message: { + id: 'components.Input.error.contain.number', + defaultMessage: 'Password must contain at least 1 number', + }, + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'Password is required', + }), confirmPassword: yup .string() - .oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch') - .required(translatedErrors.required), + .required({ + id: translatedErrors.required, + defaultMessage: 'Confirm password is required', + }) + .oneOf([yup.ref('password'), null], { + id: 'components.Input.error.password.noMatch', + defaultMessage: 'Passwords must match', + }), + email: yup + .string() + .email({ + id: translatedErrors.email, + defaultMessage: 'Not a valid email', + }) + .strict() + .lowercase({ + id: translatedErrors.lowercase, + defaultMessage: 'Email must be lowercase', + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'Email is required', + }), }); interface RegisterProps { @@ -105,8 +165,6 @@ interface RegisterFormValues { const Register = ({ hasAdmin }: RegisterProps) => { const toggleNotification = useNotification(); const navigate = useNavigate(); - const [passwordShown, setPasswordShown] = React.useState(false); - const [confirmPasswordShown, setConfirmPasswordShown] = React.useState(false); const [submitCount, setSubmitCount] = React.useState(0); const [apiError, setApiError] = React.useState(); const { trackUsage } = useTracking(); @@ -145,7 +203,7 @@ const Register = ({ hasAdmin }: RegisterProps) => { const handleRegisterAdmin = async ( { news, ...body }: RegisterAdmin.Request['body'] & { news: boolean }, - setFormErrors: FormikHelpers['setErrors'] + setFormErrors: FormHelpers['setErrors'] ) => { const res = await registerAdmin(body); @@ -191,7 +249,7 @@ const Register = ({ hasAdmin }: RegisterProps) => { const handleRegisterUser = async ( { news, ...body }: RegisterUser.Request['body'] & { news: boolean }, - setFormErrors: FormikHelpers['setErrors'] + setFormErrors: FormHelpers['setErrors'] ) => { const res = await registerUser(body); @@ -259,8 +317,8 @@ const Register = ({ hasAdmin }: RegisterProps) => { ) : null} - { news: false, } satisfies RegisterFormValues } - onSubmit={async (data, formik) => { + onSubmit={async (data, helpers) => { const normalizedData = normalizeData(data); try { @@ -294,200 +352,131 @@ const Register = ({ hasAdmin }: RegisterProps) => { registrationToken: normalizedData.registrationToken, news: normalizedData.news, }, - formik.setErrors + helpers.setErrors ); } else { await handleRegisterAdmin( omit(normalizedData, ['registrationToken', 'confirmPassword']), - formik.setErrors + helpers.setErrors ); } } catch (err) { if (err instanceof ValidationError) { - const errors = getYupInnerErrors(err); - - formik.setErrors(errors); + helpers.setErrors( + err.inner.reduce>((acc, { message, path }) => { + if (path && typeof message === 'object') { + acc[path] = formatMessage(message); + } + return acc; + }, {}) + ); } setSubmitCount(submitCount + 1); } }} - validateOnChange={false} > - {({ values, errors, handleChange }) => { - return ( -
-
- - - - + + {[ + { + label: formatMessage({ + id: 'Auth.form.firstname.label', + defaultMessage: 'Firstname', + }), + name: 'firstname', + required: true, + size: 6, + type: 'string' as const, + }, + { + label: formatMessage({ + id: 'Auth.form.lastname.label', + defaultMessage: 'Lastname', + }), + name: 'lastname', + size: 6, + type: 'string' as const, + }, + { + disabled: !isAdminRegistration, + label: formatMessage({ + id: 'Auth.form.email.label', + defaultMessage: 'Email', + }), + name: 'email', + required: true, + size: 12, + type: 'email' as const, + }, + { + hint: formatMessage({ + id: 'Auth.form.password.hint', + defaultMessage: + 'Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number', + }), + label: formatMessage({ + id: 'global.password', + defaultMessage: 'Password', + }), + name: 'password', + required: true, + size: 12, + type: 'password' as const, + }, + { + label: formatMessage({ + id: 'Auth.form.confirmPassword.label', + defaultMessage: 'Confirm Password', + }), + name: 'confirmPassword', + required: true, + size: 12, + type: 'password' as const, + }, + { + label: formatMessage( + { + id: 'Auth.form.register.news.label', + defaultMessage: + 'Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).', + }, + { + terms: ( + + {formatMessage({ + id: 'Auth.privacy-policy-agreement.terms', + defaultMessage: 'terms', })} - /> - - - + ), + policy: ( + + {formatMessage({ + id: 'Auth.privacy-policy-agreement.policy', + defaultMessage: 'policy', })} - /> - - - - { - e.preventDefault(); - setPasswordShown((prev) => !prev); - }} - label={formatMessage( - passwordShown - ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } - : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } - )} - > - {passwordShown ? : } - - } - hint={formatMessage({ - id: 'Auth.form.password.hint', - defaultMessage: - 'Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number', - })} - required - label={formatMessage({ - id: 'global.password', - defaultMessage: 'Password', - })} - type={passwordShown ? 'text' : 'password'} - /> - { - e.preventDefault(); - setConfirmPasswordShown((prev) => !prev); - }} - label={formatMessage( - confirmPasswordShown - ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } - : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } - )} - > - {confirmPasswordShown ? : } - - } - required - label={formatMessage({ - id: 'Auth.form.confirmPassword.label', - defaultMessage: 'Confirm Password', - })} - type={confirmPasswordShown ? 'text' : 'password'} - /> - { - handleChange({ target: { value: checked, name: 'news' } }); - }} - value={values.news} - name="news" - aria-label="news" - > - {formatMessage( - { - id: 'Auth.form.register.news.label', - defaultMessage: - 'Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).', - }, - { - terms: ( - - {formatMessage({ - id: 'Auth.privacy-policy-agreement.terms', - defaultMessage: 'terms', - })} - - ), - policy: ( - - {formatMessage({ - id: 'Auth.privacy-policy-agreement.policy', - defaultMessage: 'policy', - })} - - ), - } - )} - - - -
-
- ); - }} -
+ + ), + } + ), + name: 'news', + size: 12, + type: 'checkbox' as const, + }, + ].map(({ size, ...field }) => ( + + + + ))} + + +
+ {match?.params.authType === 'register' && ( @@ -560,11 +549,5 @@ const A = styled.a` color: ${({ theme }) => theme.colors.primary600}; `; -const PasswordInput = styled(TextInput)` - ::-ms-reveal { - display: none; - } -`; - export { Register }; export type { RegisterProps }; diff --git a/packages/core/admin/admin/src/pages/Auth/components/ResetPassword.tsx b/packages/core/admin/admin/src/pages/Auth/components/ResetPassword.tsx index 1478343852..16d470d3a8 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/ResetPassword.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/ResetPassword.tsx @@ -1,16 +1,13 @@ -import * as React from 'react'; - -import { Box, Button, Flex, Main, TextInput, Typography } from '@strapi/design-system'; +import { Box, Button, Flex, Main, Typography } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; -import { Form, translatedErrors, useAPIErrorHandler, useQuery } from '@strapi/helper-plugin'; -import { Eye, EyeStriked } from '@strapi/icons'; -import { Formik } from 'formik'; +import { translatedErrors, useAPIErrorHandler, useQuery } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; -import { NavLink, Navigate, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; +import { NavLink, useNavigate, Navigate } from 'react-router-dom'; import * as yup from 'yup'; import { ResetPassword } from '../../../../../shared/contracts/authentication'; +import { Form } from '../../../components/Form'; +import { InputRenderer } from '../../../components/FormInputs/Renderer'; import { Logo } from '../../../components/UnauthenticatedLogo'; import { useAuth } from '../../../features/Auth'; import { @@ -21,25 +18,49 @@ import { import { useResetPasswordMutation } from '../../../services/auth'; import { isBaseQueryError } from '../../../utils/baseQuery'; -import { FieldActionWrapper } from './FieldActionWrapper'; - const RESET_PASSWORD_SCHEMA = yup.object().shape({ password: yup .string() - .min(8, translatedErrors.minLength) - .matches(/[a-z]/, 'components.Input.error.contain.lowercase') - .matches(/[A-Z]/, 'components.Input.error.contain.uppercase') - .matches(/\d/, 'components.Input.error.contain.number') - .required(translatedErrors.required), + .min(8, { + id: translatedErrors.minLength, + defaultMessage: 'Password must be at least 8 characters', + values: { min: 8 }, + }) + .matches(/[a-z]/, { + message: { + id: 'components.Input.error.contain.lowercase', + defaultMessage: 'Password must contain at least 1 lowercase letter', + }, + }) + .matches(/[A-Z]/, { + message: { + id: 'components.Input.error.contain.uppercase', + defaultMessage: 'Password must contain at least 1 uppercase letter', + }, + }) + .matches(/\d/, { + message: { + id: 'components.Input.error.contain.number', + defaultMessage: 'Password must contain at least 1 number', + }, + }) + .required({ + id: translatedErrors.required, + defaultMessage: 'Password is required', + }), confirmPassword: yup .string() - .oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch') - .required(translatedErrors.required), + .required({ + id: translatedErrors.required, + defaultMessage: 'Confirm password is required', + }) + .oneOf([yup.ref('password'), null], { + id: 'components.Input.error.password.noMatch', + defaultMessage: 'Passwords must match', + }), }); const ResetPassword = () => { - const [passwordShown, setPasswordShown] = React.useState(false); - const [confirmPasswordShown, setConfirmPasswordShown] = React.useState(false); const { formatMessage } = useIntl(); const navigate = useNavigate(); const query = useQuery(); @@ -90,8 +111,8 @@ const ResetPassword = () => { ) : null} - { handleSubmit({ password: values.password, resetPasswordToken: query.get('code')! }); }} validationSchema={RESET_PASSWORD_SCHEMA} - validateOnChange={false} > - {({ values, errors, handleChange }) => ( -
- - { - e.preventDefault(); - setPasswordShown((prev) => !prev); - }} - label={formatMessage( - passwordShown - ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } - : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } - )} - > - {passwordShown ? : } - - } - hint={formatMessage({ - id: 'Auth.form.password.hint', - defaultMessage: - 'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number', - })} - required - label={formatMessage({ - id: 'global.password', - defaultMessage: 'Password', - })} - type={passwordShown ? 'text' : 'password'} - /> - { - e.preventDefault(); - setConfirmPasswordShown((prev) => !prev); - }} - label={formatMessage( - passwordShown - ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } - : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } - )} - > - {confirmPasswordShown ? : } - - } - required - label={formatMessage({ - id: 'Auth.form.confirmPassword.label', - defaultMessage: 'Confirm Password', - })} - type={confirmPasswordShown ? 'text' : 'password'} - /> - - -
- )} -
+ + {[ + { + hint: formatMessage({ + id: 'Auth.form.password.hint', + defaultMessage: + 'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number', + }), + label: formatMessage({ + id: 'global.password', + defaultMessage: 'Password', + }), + name: 'password', + required: true, + type: 'password' as const, + }, + { + label: formatMessage({ + id: 'Auth.form.confirmPassword.label', + defaultMessage: 'Confirm Password', + }), + name: 'confirmPassword', + required: true, + type: 'password' as const, + }, + ].map((field) => ( + + ))} + + + @@ -220,10 +173,4 @@ const ResetPassword = () => { ); }; -const PasswordInput = styled(TextInput)` - ::-ms-reveal { - display: none; - } -`; - export { ResetPassword }; diff --git a/packages/core/admin/admin/src/pages/Auth/components/tests/ForgotPassword.test.tsx b/packages/core/admin/admin/src/pages/Auth/components/tests/ForgotPassword.test.tsx index 6319dcd9ef..6e86866a54 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/tests/ForgotPassword.test.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/tests/ForgotPassword.test.tsx @@ -27,7 +27,7 @@ describe('ResetPassword', () => { fireEvent.click(getByRole('button', { name: 'Send Email' })); - expect(await findByText('This email is invalid.')).toBeInTheDocument(); + expect(await findByText('This is not a valid email.')).toBeInTheDocument(); }); }); }); diff --git a/packages/core/admin/admin/src/pages/Auth/components/tests/Register.test.tsx b/packages/core/admin/admin/src/pages/Auth/components/tests/Register.test.tsx index 19242194eb..1ca9ead115 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/tests/Register.test.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/tests/Register.test.tsx @@ -26,7 +26,7 @@ describe('Register', () => { /keep me updated about new features & upcoming improvements \(by doing this you accept the and the \)\./i ) ).toBeInTheDocument(); - expect(getByRole('checkbox', { name: /news/i })).toBeInTheDocument(); + expect(getByRole('checkbox', { name: /Keep me updated/i })).toBeInTheDocument(); expect(getByRole('button', { name: /let's start/i })).toBeInTheDocument(); }); diff --git a/packages/core/admin/admin/src/pages/Auth/components/tests/ResetPassword.test.tsx b/packages/core/admin/admin/src/pages/Auth/components/tests/ResetPassword.test.tsx index eda4e695b3..6fe71b1b5a 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/tests/ResetPassword.test.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/tests/ResetPassword.test.tsx @@ -31,7 +31,7 @@ describe('ResetPassword', () => { fireEvent.click(getByRole('button', { name: 'Change password' })); - expect(await findByText('This value is required.')).toBeInTheDocument(); + expect(await findByText('Passwords must match')).toBeInTheDocument(); }); it('should fail if we do not fill in the password field', async () => { @@ -43,7 +43,8 @@ describe('ResetPassword', () => { fireEvent.click(getByRole('button', { name: 'Change password' })); - expect(await findByText('This value is required.')).toBeInTheDocument(); + expect(await findByText('Password must be at least 8 characters')).toBeInTheDocument(); + expect(await findByText('Passwords must match')).toBeInTheDocument(); }); it('should fail if the passwords do not match', async () => { @@ -56,7 +57,7 @@ describe('ResetPassword', () => { fireEvent.click(getByRole('button', { name: 'Change password' })); - expect(await findByText('This value is required.')).toBeInTheDocument(); + expect(await findByText('Passwords must match')).toBeInTheDocument(); }); }); }); diff --git a/packages/core/admin/admin/src/pages/ProfilePage.tsx b/packages/core/admin/admin/src/pages/ProfilePage.tsx index 934c54b887..7a2453d90c 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage.tsx +++ b/packages/core/admin/admin/src/pages/ProfilePage.tsx @@ -17,7 +17,6 @@ import { FieldAction, } from '@strapi/design-system'; import { - Form, GenericInput, GenericInputProps, LoadingIndicatorPage, @@ -30,7 +29,7 @@ import { useAPIErrorHandler, } from '@strapi/helper-plugin'; import { Check, Eye, EyeStriked } from '@strapi/icons'; -import { Formik, FormikHelpers } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import upperFirst from 'lodash/upperFirst'; import { Helmet } from 'react-helmet'; import { useIntl } from 'react-intl'; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx index a3287d0a22..b595628375 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { ContentLayout, Flex, Main } from '@strapi/design-system'; import { CheckPagePermissions, - Form, SettingsPageTitle, useAPIErrorHandler, useFocusWhenNavigate, @@ -13,7 +12,7 @@ import { useRBAC, useTracking, } from '@strapi/helper-plugin'; -import { Formik, FormikHelpers } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import { useIntl } from 'react-intl'; import { useLocation, useMatch, useNavigate } from 'react-router-dom'; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Roles/CreatePage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Roles/CreatePage.tsx index 9524157685..abeb0fdf1a 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Roles/CreatePage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Roles/CreatePage.tsx @@ -16,7 +16,6 @@ import { import { Link } from '@strapi/design-system/v2'; import { CheckPagePermissions, - Form, LoadingIndicatorPage, SettingsPageTitle, useNotification, @@ -27,7 +26,7 @@ import { } from '@strapi/helper-plugin'; import { ArrowLeft } from '@strapi/icons'; import { format } from 'date-fns'; -import { Formik, FormikHelpers } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import { useIntl } from 'react-intl'; import { NavLink, useNavigate, useMatch } from 'react-router-dom'; import styled from 'styled-components'; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx index 9bf70095b3..c763647355 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx @@ -13,7 +13,6 @@ import { } from '@strapi/design-system'; import { CheckPagePermissions, - Form, LoadingIndicatorPage, SettingsPageTitle, useAPIErrorHandler, @@ -26,7 +25,7 @@ import { translatedErrors, } from '@strapi/helper-plugin'; import { Check } from '@strapi/icons'; -import { Formik, FormikErrors, FormikHelpers } from 'formik'; +import { Formik, Form, FormikErrors, FormikHelpers } from 'formik'; import { useIntl } from 'react-intl'; import { useLocation, useNavigate, useMatch } from 'react-router-dom'; import * as yup from 'yup'; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/EditPage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/EditPage.tsx index 5dbde185b3..cffa50dded 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/EditPage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/EditPage.tsx @@ -13,7 +13,6 @@ import { } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; import { - Form, GenericInput, LoadingIndicatorPage, SettingsPageTitle, @@ -25,8 +24,7 @@ import { useRBAC, } from '@strapi/helper-plugin'; import { ArrowLeft, Check } from '@strapi/icons'; -import { Formik, FormikHelpers } from 'formik'; -import omit from 'lodash/omit'; +import { Formik, Form, FormikHelpers } from 'formik'; import pick from 'lodash/pick'; import { useIntl } from 'react-intl'; import { NavLink, Navigate, useLocation, useMatch, useNavigate } from 'react-router-dom'; @@ -177,13 +175,10 @@ const EditPage = () => { confirmPassword: '', } satisfies InitialData; - /** - * TODO: Convert this to react-query. - */ const handleSubmit = async (body: InitialData, actions: FormikHelpers) => { lockApp?.(); - const { confirmPassword, password, ...bodyRest } = body; + const { confirmPassword: _confirmPassword, password, ...bodyRest } = body; const res = await updateUser({ id, diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx index d26b11f132..8204c82cf5 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx @@ -14,7 +14,6 @@ import { } from '@strapi/design-system'; import { Breadcrumbs, Crumb } from '@strapi/design-system/v2'; import { - Form, GenericInput, useNotification, useOverlayBlocker, @@ -22,7 +21,7 @@ import { useAPIErrorHandler, } from '@strapi/helper-plugin'; import { Entity } from '@strapi/types'; -import { Formik, FormikHelpers } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import { useIntl } from 'react-intl'; import * as yup from 'yup'; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/WebhookForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/WebhookForm.tsx index 8fd1043b05..10d05615f2 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/WebhookForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/WebhookForm.tsx @@ -11,10 +11,9 @@ import { TextInput, } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; -import { Form } from '@strapi/helper-plugin'; import { ArrowLeft, Check, Play as Publish } from '@strapi/icons'; import { Webhook } from '@strapi/types'; -import { Field, FormikHelpers, FormikProvider, useFormik } from 'formik'; +import { Field, Form, FormikHelpers, FormikProvider, useFormik } from 'formik'; import { IntlShape, useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; import * as yup from 'yup'; diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 590b731333..34b88b0b55 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -591,7 +591,7 @@ "components.Input.error.contentTypeName.taken": "This name already exists", "components.Input.error.custom-error": "{errorMessage} ", "components.Input.error.password.noMatch": "Passwords do not match", - "components.Input.error.validation.email": "This is an invalid email", + "components.Input.error.validation.email": "This is not a valid email", "components.Input.error.validation.json": "This doesn't match the JSON format", "components.Input.error.validation.lowercase": "The value must be a lowercase string", "components.Input.error.validation.max": "The value is too high (max: {max}).", diff --git a/packages/core/admin/admin/src/content-manager/utils/object.ts b/packages/core/admin/admin/src/utils/object.ts similarity index 100% rename from packages/core/admin/admin/src/content-manager/utils/object.ts rename to packages/core/admin/admin/src/utils/object.ts diff --git a/packages/core/admin/admin/src/content-manager/utils/refs.ts b/packages/core/admin/admin/src/utils/refs.ts similarity index 100% rename from packages/core/admin/admin/src/content-manager/utils/refs.ts rename to packages/core/admin/admin/src/utils/refs.ts diff --git a/packages/core/admin/admin/src/content-manager/utils/tests/object.test.ts b/packages/core/admin/admin/src/utils/tests/object.test.ts similarity index 100% rename from packages/core/admin/admin/src/content-manager/utils/tests/object.test.ts rename to packages/core/admin/admin/src/utils/tests/object.test.ts diff --git a/packages/core/admin/admin/src/content-manager/utils/tests/refs.test.ts b/packages/core/admin/admin/src/utils/tests/refs.test.ts similarity index 100% rename from packages/core/admin/admin/src/content-manager/utils/tests/refs.test.ts rename to packages/core/admin/admin/src/utils/tests/refs.test.ts diff --git a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/AssigneeSelect.tsx b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/AssigneeSelect.tsx index 533351fa06..3d88504032 100644 --- a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/AssigneeSelect.tsx +++ b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/AssigneeSelect.tsx @@ -2,7 +2,7 @@ import { Combobox, ComboboxOption, Field, Flex } from '@strapi/design-system'; import { useAPIErrorHandler, useNotification, useRBAC } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; -import { useField } from '../../../../../../../admin/src/content-manager/components/Form'; +import { useField } from '../../../../../../../admin/src/components/Form'; import { useDoc } from '../../../../../../../admin/src/content-manager/hooks/useDocument'; import { getDisplayName } from '../../../../../../../admin/src/content-manager/utils/users'; import { useTypedSelector } from '../../../../../../../admin/src/core/store/hooks'; diff --git a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/StageSelect.tsx b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/StageSelect.tsx index 2ed7e91a7a..12eb4d3d3f 100644 --- a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/StageSelect.tsx +++ b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/StageSelect.tsx @@ -14,7 +14,7 @@ import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin'; import { Entity } from '@strapi/types'; import { useIntl } from 'react-intl'; -import { useField } from '../../../../../../../admin/src/content-manager/components/Form'; +import { useField } from '../../../../../../../admin/src/components/Form'; import { useDoc } from '../../../../../../../admin/src/content-manager/hooks/useDocument'; import { useLicenseLimits } from '../../../../hooks/useLicenseLimits'; import { LimitsModal } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal'; diff --git a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/AssigneeSelect.test.tsx b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/AssigneeSelect.test.tsx index d149e3b6f1..878ea302e3 100644 --- a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/AssigneeSelect.test.tsx +++ b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/AssigneeSelect.test.tsx @@ -2,7 +2,7 @@ import { render as renderRTL, waitFor, server } from '@tests/utils'; import { rest } from 'msw'; import { Route, Routes } from 'react-router-dom'; -import { Form } from '../../../../../../../../admin/src/content-manager/components/Form'; +import { Form } from '../../../../../../../../admin/src/components/Form'; import { AssigneeSelect } from '../AssigneeSelect'; import { ASSIGNEE_ATTRIBUTE_NAME } from '../constants'; diff --git a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/StageSelect.test.tsx b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/StageSelect.test.tsx index 9f516458ac..8ab83ce780 100644 --- a/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/StageSelect.test.tsx +++ b/packages/core/admin/ee/admin/src/content-manager/pages/EditView/components/tests/StageSelect.test.tsx @@ -3,7 +3,7 @@ import { render as renderRTL, waitFor, server } from '@tests/utils'; import { rest } from 'msw'; import { Route, Routes } from 'react-router-dom'; -import { Form } from '../../../../../../../../admin/src/content-manager/components/Form'; +import { Form } from '../../../../../../../../admin/src/components/Form'; import { StageSelect } from '../StageSelect'; /** diff --git a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/ReviewWorkflows/components/Stage.tsx b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/ReviewWorkflows/components/Stage.tsx index 6092f3055d..9e0684df8d 100644 --- a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/ReviewWorkflows/components/Stage.tsx +++ b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/ReviewWorkflows/components/Stage.tsx @@ -33,7 +33,7 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { useDragAndDrop } from '../../../../../../../../admin/src/content-manager/hooks/useDragAndDrop'; -import { composeRefs } from '../../../../../../../../admin/src/content-manager/utils/refs'; +import { composeRefs } from '../../../../../../../../admin/src/utils/refs'; import { StagePermission } from '../../../../../../../../shared/contracts/review-workflows'; import { cloneStage, diff --git a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/SingleSignOnPage.tsx b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/SingleSignOnPage.tsx index 3ca13bd032..90f8084d33 100644 --- a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/SingleSignOnPage.tsx +++ b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/SingleSignOnPage.tsx @@ -16,7 +16,6 @@ import { } from '@strapi/design-system'; import { CheckPagePermissions, - Form, LoadingIndicatorPage, SettingsPageTitle, translatedErrors, @@ -27,7 +26,7 @@ import { useRBAC, } from '@strapi/helper-plugin'; import { Check } from '@strapi/icons'; -import { Formik, FormikHelpers } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import { useIntl } from 'react-intl'; import * as yup from 'yup'; diff --git a/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx b/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx index ffffe21844..6d9b4a5331 100644 --- a/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx +++ b/packages/core/content-releases/admin/src/components/tests/CMReleasesContainer.test.tsx @@ -138,7 +138,7 @@ describe('CMReleasesContainer', () => { }) ); - render(); + render(); const informationBox = await screen.findByRole('complementary', { name: 'Releases' }); const release1 = await within(informationBox).findByText('01/01/2024 at 11:00 (UTC+01:00)'); diff --git a/packages/core/email/admin/src/pages/Settings.tsx b/packages/core/email/admin/src/pages/Settings.tsx index 7c05ea58b7..fa503ca0be 100644 --- a/packages/core/email/admin/src/pages/Settings.tsx +++ b/packages/core/email/admin/src/pages/Settings.tsx @@ -290,7 +290,7 @@ const SettingsPage = () => { formErrors.email?.id && formatMessage({ id: `email.${formErrors.email?.id}`, - defaultMessage: 'This is an invalid email', + defaultMessage: 'This is not a valid email', }) } placeholder={formatMessage({ diff --git a/packages/core/email/admin/src/translations/en.json b/packages/core/email/admin/src/translations/en.json index fa7e87a87b..28b1942d6c 100644 --- a/packages/core/email/admin/src/translations/en.json +++ b/packages/core/email/admin/src/translations/en.json @@ -19,5 +19,5 @@ "Settings.email.plugin.title.test": "Test email delivery", "SettingsNav.link.settings": "Settings", "SettingsNav.section-label": "Email plugin", - "components.Input.error.validation.email": "This is an invalid email" + "components.Input.error.validation.email": "This is not a valid email" } diff --git a/packages/core/helper-plugin/MIGRATION_GUIDE.md b/packages/core/helper-plugin/MIGRATION_GUIDE.md index a3e691fff1..d073ab5b62 100644 --- a/packages/core/helper-plugin/MIGRATION_GUIDE.md +++ b/packages/core/helper-plugin/MIGRATION_GUIDE.md @@ -17,6 +17,20 @@ import { DateTimePicker } from '@strapi/helper-plugin'; import { DateTimePicker } from '@strapi/design-system'; ``` +### Form + +This component aliased `Formik`, something we're working towards removing. The `Form` component and it's sibling exports from `@strapi/strapi/admin` should be used instead: + +```ts +// Before +import { Form } from '@strapi/helper-plugin'; + +// After +import { Form } from '@strapi/strapi/admin'; +``` + +Users should note that any use of the Formik library will no longer work & insted should look at the documentation for the `Form` component. + ### Link This was aliasing the design-system and using the `as` prop with `react-router-dom`. You should import the component from there: diff --git a/packages/core/helper-plugin/src/components/Form.tsx b/packages/core/helper-plugin/src/components/Form.tsx deleted file mode 100644 index 493d2f833c..0000000000 --- a/packages/core/helper-plugin/src/components/Form.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react'; - -import { Form as FormikForm, FormikFormProps, useFormikContext } from 'formik'; - -export type FormProps = Omit; - -/** - * @deprecated Use Formik form directly instead. - */ -const Form = ({ ...props }: FormProps) => { - const formRef = React.useRef(null!); - const { isSubmitting, isValidating, errors, touched } = useFormikContext(); - - React.useEffect(() => { - if (isSubmitting && !isValidating) { - const errorsInForm = formRef.current.querySelectorAll('[data-strapi-field-error]'); - - if (errorsInForm && errorsInForm.length > 0) { - const firstError = errorsInForm[0]; - const describingId = firstError.getAttribute('id'); - const formElementInError = formRef.current.querySelector( - `[aria-describedby="${describingId}"]` - ); - - if (formElementInError && formElementInError instanceof HTMLElement) { - formElementInError.focus(); - } - } - } - - if (!isSubmitting && !isValidating && Object.keys(errors).length) { - const el = document.getElementById('global-form-error'); - - if (el) { - el.focus(); - } - } - }, [errors, isSubmitting, isValidating, touched]); - - return ; -}; - -export { Form }; diff --git a/packages/core/helper-plugin/src/components/NotAllowedInput.tsx b/packages/core/helper-plugin/src/components/NotAllowedInput.tsx index 54a1d30823..ad9d98420a 100644 --- a/packages/core/helper-plugin/src/components/NotAllowedInput.tsx +++ b/packages/core/helper-plugin/src/components/NotAllowedInput.tsx @@ -10,7 +10,7 @@ import type { Attribute } from '@strapi/types'; interface NotAllowedInputProps { disabled?: boolean; hint?: ReactNode; - label: string; + label: ReactNode; name: string; placeholder?: string; required?: boolean; @@ -29,6 +29,7 @@ const NotAllowedInput = ({ hint, label, required, name }: NotAllowedInputProps) return ( - -# PageTemplate - -This component is used to display an empty state. - -## Imports - -```js -import { Main } from '@strapi/design-system'; -import { ActionLayout, ContentLayout, HeaderLayout, Layout } from '@strapi/design-system'; -import { Button } from '@strapi/design-system'; -import { Box } from '@strapi/design-system'; -import { LoadingIndicatorPage, NoContent, NoPermissions } from '@strapi/helper-plugin'; -``` - -## Usage - - - - {() => { - const canRead = false; - const isLoading = true; - const data = []; - return ( - - -
- }>Add an entry} - secondaryAction={ - - } - title="Other CT" - subtitle="36 entries found" - /> - - - - - } - endActions={ - <> - - - } - /> - - {!canRead && } - {canRead && data && data.length === 0 && ( - Add content} /> - )} - {isLoading && } - -
-
-
- ); - }} -
-
diff --git a/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.jsx b/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.jsx index 8168054e10..ed78c81160 100644 --- a/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.jsx +++ b/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.jsx @@ -13,8 +13,8 @@ import { ModalLayout, Typography, } from '@strapi/design-system'; -import { Form, normalizeAPIError } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { normalizeAPIError } from '@strapi/helper-plugin'; +import { Form, Formik } from 'formik'; import isEmpty from 'lodash/isEmpty'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/core/upload/admin/src/components/EditAssetDialog/index.jsx b/packages/core/upload/admin/src/components/EditAssetDialog/index.jsx index e134b42e8a..9d5abdf99d 100644 --- a/packages/core/upload/admin/src/components/EditAssetDialog/index.jsx +++ b/packages/core/upload/admin/src/components/EditAssetDialog/index.jsx @@ -19,8 +19,8 @@ import { TextInput, VisuallyHidden, } from '@strapi/design-system'; -import { Form, getFileExtension, pxToRem, useTracking } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { getFileExtension, pxToRem, useTracking } from '@strapi/helper-plugin'; +import { Form, Formik } from 'formik'; import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.jsx b/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.jsx index 827ebdd45c..cfcca7af38 100644 --- a/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.jsx +++ b/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.jsx @@ -13,8 +13,8 @@ import { TextInput, Typography, } from '@strapi/design-system'; -import { Form, getAPIInnerErrors, useNotification, useTracking } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { getAPIInnerErrors, useNotification, useTracking } from '@strapi/helper-plugin'; +import { Form, Formik } from 'formik'; import isEmpty from 'lodash/isEmpty'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/core/upload/admin/src/components/UploadAssetDialog/AddAssetStep/FromUrlForm.jsx b/packages/core/upload/admin/src/components/UploadAssetDialog/AddAssetStep/FromUrlForm.jsx index 7d5d9df1ec..48b21b14e2 100644 --- a/packages/core/upload/admin/src/components/UploadAssetDialog/AddAssetStep/FromUrlForm.jsx +++ b/packages/core/upload/admin/src/components/UploadAssetDialog/AddAssetStep/FromUrlForm.jsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import { Box, Button, ModalFooter, Textarea } from '@strapi/design-system'; -import { Form, useTracking } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { useTracking } from '@strapi/helper-plugin'; +import { Form, Formik } from 'formik'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/plugins/documentation/admin/src/pages/SettingsPage/index.jsx b/packages/plugins/documentation/admin/src/pages/SettingsPage/index.jsx index cce2d5283d..94176336cf 100644 --- a/packages/plugins/documentation/admin/src/pages/SettingsPage/index.jsx +++ b/packages/plugins/documentation/admin/src/pages/SettingsPage/index.jsx @@ -15,7 +15,6 @@ import { FieldAction, } from '@strapi/design-system'; import { - Form, LoadingIndicatorPage, useFocusWhenNavigate, translatedErrors, @@ -23,7 +22,7 @@ import { } from '@strapi/helper-plugin'; // Strapi Icons import { Check, Eye as Show, EyeStriked as Hide } from '@strapi/icons'; -import { Formik } from 'formik'; +import { Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; import * as yup from 'yup'; diff --git a/packages/plugins/i18n/admin/src/components/CreateLocale.tsx b/packages/plugins/i18n/admin/src/components/CreateLocale.tsx index e469c61e92..4a555a1451 100644 --- a/packages/plugins/i18n/admin/src/components/CreateLocale.tsx +++ b/packages/plugins/i18n/admin/src/components/CreateLocale.tsx @@ -415,6 +415,7 @@ const EnumerationInput = ({ disabled={disabled} error={error} hint={hint} + // @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS. label={label} name={name} // @ts-expect-error – This will dissapear when the DS removes support for numbers to be returned by SingleSelect. diff --git a/packages/plugins/users-permissions/admin/src/components/FormModal/index.jsx b/packages/plugins/users-permissions/admin/src/components/FormModal/index.jsx index 83c059252b..c1823d7ddb 100644 --- a/packages/plugins/users-permissions/admin/src/components/FormModal/index.jsx +++ b/packages/plugins/users-permissions/admin/src/components/FormModal/index.jsx @@ -17,8 +17,7 @@ import { ModalLayout, } from '@strapi/design-system'; import { Breadcrumbs, Crumb } from '@strapi/design-system/v2'; -import { Form } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { Form, Formik } from 'formik'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/plugins/users-permissions/admin/src/pages/AdvancedSettings/index.jsx b/packages/plugins/users-permissions/admin/src/pages/AdvancedSettings/index.jsx index 37a99d2083..f06e905dba 100644 --- a/packages/plugins/users-permissions/admin/src/pages/AdvancedSettings/index.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/AdvancedSettings/index.jsx @@ -16,7 +16,6 @@ import { } from '@strapi/design-system'; import { CheckPagePermissions, - Form, GenericInput, LoadingIndicatorPage, SettingsPageTitle, @@ -28,7 +27,7 @@ import { useRBAC, } from '@strapi/helper-plugin'; import { Check } from '@strapi/icons'; -import { Formik } from 'formik'; +import { Formik, Form } from 'formik'; import { useIntl } from 'react-intl'; import { useMutation, useQuery, useQueryClient } from 'react-query'; diff --git a/packages/plugins/users-permissions/admin/src/pages/EmailTemplates/components/EmailForm.jsx b/packages/plugins/users-permissions/admin/src/pages/EmailTemplates/components/EmailForm.jsx index 5d981227d1..666f852f5e 100644 --- a/packages/plugins/users-permissions/admin/src/pages/EmailTemplates/components/EmailForm.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/EmailTemplates/components/EmailForm.jsx @@ -11,8 +11,8 @@ import { Textarea, } from '@strapi/design-system'; import { Breadcrumbs, Crumb } from '@strapi/design-system/v2'; -import { Form, GenericInput } from '@strapi/helper-plugin'; -import { Formik } from 'formik'; +import { GenericInput } from '@strapi/helper-plugin'; +import { Form, Formik } from 'formik'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; diff --git a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/CreatePage.jsx b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/CreatePage.jsx index 606e0ccdba..d5f1422f21 100644 --- a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/CreatePage.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/CreatePage.jsx @@ -14,7 +14,6 @@ import { } from '@strapi/design-system'; import { CheckPagePermissions, - Form, SettingsPageTitle, useFetchClient, useNotification, @@ -22,7 +21,7 @@ import { useTracking, } from '@strapi/helper-plugin'; import { Check } from '@strapi/icons'; -import { Formik } from 'formik'; +import { Formik, Form } from 'formik'; import { useIntl } from 'react-intl'; import { useMutation } from 'react-query'; import { useNavigate } from 'react-router-dom'; diff --git a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/EditPage.jsx b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/EditPage.jsx index dd2df8df3a..b5bc72d92d 100644 --- a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/EditPage.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/EditPage.jsx @@ -18,13 +18,12 @@ import { useOverlayBlocker, SettingsPageTitle, LoadingIndicatorPage, - Form, useAPIErrorHandler, useFetchClient, useNotification, } from '@strapi/helper-plugin'; import { ArrowLeft, Check } from '@strapi/icons'; -import { Formik } from 'formik'; +import { Formik, Form } from 'formik'; import { useIntl } from 'react-intl'; import { useQuery, useMutation } from 'react-query'; import { NavLink, useMatch } from 'react-router-dom'; @@ -110,7 +109,7 @@ export const EditPage = () => {
{ defaultMessage: 'Save', })} - ) + ) : null } title={role.name} subtitle={role.description} diff --git a/packages/plugins/users-permissions/admin/src/translations/en.json b/packages/plugins/users-permissions/admin/src/translations/en.json index 62f043840e..874a8482c0 100644 --- a/packages/plugins/users-permissions/admin/src/translations/en.json +++ b/packages/plugins/users-permissions/admin/src/translations/en.json @@ -60,7 +60,7 @@ "Settings.roles.deleted": "Role deleted", "Settings.roles.edited": "Role edited", "Settings.section-label": "Users & Permissions plugin", - "components.Input.error.validation.email": "This is an invalid email", + "components.Input.error.validation.email": "This is not a valid email", "components.Input.error.validation.json": "This doesn't match the JSON format", "components.Input.error.validation.max": "The value is too high.", "components.Input.error.validation.maxLength": "The value is too long.", From b0e5eb239896779b25958c31c12f2b34087238f7 Mon Sep 17 00:00:00 2001 From: markkaylor Date: Fri, 1 Mar 2024 09:08:12 +0100 Subject: [PATCH 07/14] feat(dts): support models and contentTypes (#19604) --- packages/core/data-transfer/package.json | 1 + .../data-transfer/src/__tests__/test-utils.ts | 7 ++ .../local-destination/__tests__/index.test.ts | 34 +++++++- .../__tests__/restore.test.ts | 79 +++++++++++++++-- .../strategies/restore/index.ts | 86 +++++++++++++------ yarn.lock | 3 +- 6 files changed, 175 insertions(+), 35 deletions(-) diff --git a/packages/core/data-transfer/package.json b/packages/core/data-transfer/package.json index 910c924d20..e5f6fab5b6 100644 --- a/packages/core/data-transfer/package.json +++ b/packages/core/data-transfer/package.json @@ -59,6 +59,7 @@ "ws": "8.13.0" }, "devDependencies": { + "@strapi/database": "workspace:*", "@strapi/pack-up": "4.20.3", "@types/fs-extra": "9.0.13", "@types/jest": "29.5.2", diff --git a/packages/core/data-transfer/src/__tests__/test-utils.ts b/packages/core/data-transfer/src/__tests__/test-utils.ts index 5407f60d83..e10d0e614b 100644 --- a/packages/core/data-transfer/src/__tests__/test-utils.ts +++ b/packages/core/data-transfer/src/__tests__/test-utils.ts @@ -49,6 +49,13 @@ export const getContentTypes = (): { bar: { uid: 'bar', attributes: { age: { type: 'number' } } }, }); +/** + * Factory to get default strapi models test values + */ +export const getStrapiModels = () => { + return [{ uid: 'model::foo' }, { uid: 'model::bar' }]; +}; + /** * Create a factory of readable streams (wrapped with a jest mock function) */ diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/index.test.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/index.test.ts index 3c5f417a05..df9f7f3964 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/index.test.ts @@ -4,6 +4,7 @@ import { getStrapiFactory, getContentTypes, setGlobalStrapi, + getStrapiModels, } from '../../../../__tests__/test-utils'; afterEach(() => { @@ -140,15 +141,37 @@ describe('Local Strapi Source Destination', () => { entity: { id: 9, age: 0 }, contentType: { uid: 'bar' }, }, + { + entity: { id: 10, age: 0 }, + model: { uid: 'model::foo' }, + }, + { + entity: { id: 11, age: 0 }, + model: { uid: 'model::bar' }, + }, ]; const deleteMany = (uid: string) => jest.fn(async () => ({ - count: entities.filter((entity) => entity.contentType.uid === uid).length, + count: entities.filter((entity) => { + if (entity.model) { + return entity.model.uid === uid; + } + + return entity.contentType.uid === uid; + }).length, })); const findMany = (uid: string) => { - return jest.fn(async () => entities.filter((entity) => entity.contentType.uid === uid)); + return jest.fn(async () => + entities.filter((entity) => { + if (entity.model) { + return entity.model.uid === uid; + } + + return entity.contentType.uid === uid; + }) + ); }; const query = jest.fn((uid) => { @@ -164,6 +187,13 @@ describe('Local Strapi Source Destination', () => { contentTypes: getContentTypes(), query, getModel, + get() { + return { + get() { + return getStrapiModels(); + }, + }; + }, db: { query, transaction, diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/restore.test.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/restore.test.ts index 282587fcbd..203682a470 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/restore.test.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/__tests__/restore.test.ts @@ -4,6 +4,7 @@ import { getStrapiFactory, getContentTypes, setGlobalStrapi, + getStrapiModels, } from '../../../../__tests__/test-utils'; import { IConfiguration } from '../../../../../types'; @@ -36,6 +37,14 @@ const entities = [ entity: { id: 9, age: 0 }, contentType: { uid: 'bar' }, }, + { + entity: { id: 10, age: 0 }, + model: { uid: 'model::foo' }, + }, + { + entity: { id: 11, age: 0 }, + model: { uid: 'model::bar' }, + }, ]; afterEach(() => { @@ -44,11 +53,25 @@ afterEach(() => { const deleteMany = (uid: string) => jest.fn(async () => ({ - count: entities.filter((entity) => entity.contentType.uid === uid).length, + count: entities.filter((entity) => { + if (entity.model) { + return entity.model.uid === uid; + } + + return entity.contentType.uid === uid; + }).length, })); const findMany = (uid: string) => { - return jest.fn(async () => entities.filter((entity) => entity.contentType.uid === uid)); + return jest.fn(async () => + entities.filter((entity) => { + if (entity.model) { + return entity.model.uid === uid; + } + + return entity.contentType.uid === uid; + }) + ); }; const create = jest.fn((data) => data); @@ -64,12 +87,21 @@ const query = jest.fn((uid) => { }); describe('Restore ', () => { - test('Should delete all contentTypes', async () => { + test('Should delete all models and contentTypes', async () => { const strapi = getStrapiFactory({ contentTypes: getContentTypes(), query, getModel, - db: { query }, + get() { + return { + get() { + return getStrapiModels(); + }, + }; + }, + db: { + query, + }, })(); setGlobalStrapi(strapi); @@ -83,7 +115,16 @@ describe('Restore ', () => { contentTypes: getContentTypes(), query, getModel, - db: { query }, + get() { + return { + get() { + return getStrapiModels(); + }, + }; + }, + db: { + query, + }, })(); setGlobalStrapi(strapi); @@ -96,6 +137,34 @@ describe('Restore ', () => { expect(count).toBe(3); }); + test('Should only delete chosen model ', async () => { + const strapi = getStrapiFactory({ + contentTypes: getContentTypes(), + query, + getModel, + get() { + return { + get() { + return getStrapiModels(); + }, + }; + }, + db: { + query, + }, + })(); + + setGlobalStrapi(strapi); + + const { count } = await deleteRecords(strapi, { + entities: { + include: ['model::foo'], + }, + }); + + expect(count).toBe(1); + }); + test('Should add core store data', async () => { const strapi = getStrapiFactory({ contentTypes: getContentTypes(), diff --git a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/index.ts b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/index.ts index 729cef60c6..e8e7939485 100644 --- a/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/index.ts +++ b/packages/core/data-transfer/src/strapi/providers/local-destination/strategies/restore/index.ts @@ -1,4 +1,5 @@ -import type { LoadedStrapi, Schema } from '@strapi/types'; +import type { LoadedStrapi, Schema, Common } from '@strapi/types'; +import type { Model } from '@strapi/database'; import { ProviderTransferError } from '../../../../../errors/providers'; import * as queries from '../../../../queries'; @@ -37,44 +38,75 @@ const deleteEntitiesRecords = async ( options: IRestoreOptions = {} ): Promise => { const { entities } = options; - const query = queries.entity.createEntityQuery(strapi); - const contentTypes = Object.values( - strapi.contentTypes as Record - ); - const contentTypesToClear = contentTypes.filter((contentType) => { - let removeThisContentType = true; + const models = strapi.get('models').get() as Model[]; + const contentTypes = Object.values(strapi.contentTypes) as Schema.ContentType[]; - // include means "only include these types" so if it's not in here, it's not being included - if (entities?.include) { - removeThisContentType = entities.include.includes(contentType.uid); - } + const contentTypesToClear = contentTypes + .filter((contentType) => { + let removeThisContentType = true; - // if something is excluded, remove it. But lack of being excluded doesn't mean it's kept - if (entities?.exclude && entities.exclude.includes(contentType.uid)) { - removeThisContentType = false; - } + // include means "only include these types" so if it's not in here, it's not being included + if (entities?.include) { + removeThisContentType = entities.include.includes(contentType.uid); + } - if (entities?.filters) { - removeThisContentType = entities.filters.every((filter) => filter(contentType)); - } + // if something is excluded, remove it. But lack of being excluded doesn't mean it's kept + if (entities?.exclude && entities.exclude.includes(contentType.uid)) { + removeThisContentType = false; + } - return removeThisContentType; - }); + if (entities?.filters) { + removeThisContentType = entities.filters.every((filter) => filter(contentType)); + } - const [results, updateResults] = useResults( - contentTypesToClear.map((contentType) => contentType.uid) - ); + return removeThisContentType; + }) + .map((contentType) => contentType.uid); - const deletePromises = contentTypesToClear.map(async (contentType) => { - const result = await query(contentType.uid).deleteMany(entities?.params); + const modelsToClear = models + .filter((model) => { + if (contentTypesToClear.includes(model.uid as Common.UID.ContentType)) { + return false; + } + + let removeThisModel = true; + + // include means "only include these types" so if it's not in here, it's not being included + if (entities?.include) { + removeThisModel = entities.include.includes(model.uid); + } + + // if something is excluded, remove it. But lack of being excluded doesn't mean it's kept + if (entities?.exclude && entities.exclude.includes(model.uid)) { + removeThisModel = false; + } + + return removeThisModel; + }) + .map((model) => model.uid); + + const [results, updateResults] = useResults([...contentTypesToClear, ...modelsToClear]); + + const contentTypeQuery = queries.entity.createEntityQuery(strapi); + + const contentTypePromises = contentTypesToClear.map(async (uid) => { + const result = await contentTypeQuery(uid).deleteMany(entities?.params); if (result) { - updateResults(result.count || 0, contentType.uid); + updateResults(result.count || 0, uid); } }); - await Promise.all(deletePromises); + const modelsPromises = modelsToClear.map(async (uid) => { + const result = await strapi.db.query(uid).deleteMany({}); + + if (result) { + updateResults(result.count || 0, uid); + } + }); + + await Promise.all([...contentTypePromises, ...modelsPromises]); return results; }; diff --git a/yarn.lock b/yarn.lock index 907b42c8b1..20438a7c2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7060,6 +7060,7 @@ __metadata: version: 0.0.0-use.local resolution: "@strapi/data-transfer@workspace:packages/core/data-transfer" dependencies: + "@strapi/database": "workspace:*" "@strapi/logger": "npm:4.20.3" "@strapi/pack-up": "npm:4.20.3" "@strapi/types": "npm:4.20.3" @@ -7096,7 +7097,7 @@ __metadata: languageName: unknown linkType: soft -"@strapi/database@npm:4.20.3, @strapi/database@workspace:packages/core/database": +"@strapi/database@npm:4.20.3, @strapi/database@workspace:*, @strapi/database@workspace:packages/core/database": version: 0.0.0-use.local resolution: "@strapi/database@workspace:packages/core/database" dependencies: From b4f7a07cbb033abd22f75d97d2a9888e22de2dca Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:39:00 +0000 Subject: [PATCH 08/14] chore(strapi): make watch-admin default on develop (#19647) --- packages/core/strapi/src/cli/commands/develop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/strapi/src/cli/commands/develop.ts b/packages/core/strapi/src/cli/commands/develop.ts index c8f003c771..a37dd01a66 100644 --- a/packages/core/strapi/src/cli/commands/develop.ts +++ b/packages/core/strapi/src/cli/commands/develop.ts @@ -33,7 +33,7 @@ const command: StrapiCommand = ({ ctx }) => { .option('--silent', "Don't log anything", false) .option('--ignore-prompts', 'Ignore all prompts', false) .option('--polling', 'Watch for file changes in network directories', false) - .option('--watch-admin', 'Watch the admin panel for hot changes', false) + .option('--watch-admin', 'Watch the admin panel for hot changes', true) .option('--open', 'Open the admin in your browser', true) .description('Start your Strapi application in development mode') .action(async (options: DevelopCLIOptions) => { From 73143c28059b343ba62d98c29672ab114562fbbc Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 1 Mar 2024 12:31:17 +0100 Subject: [PATCH 09/14] test: improve e2e playwright config --- .github/workflows/tests.yml | 4 ++-- docs/docs/guides/e2e/00-setup.md | 40 ++++++++++++++++++++++++++++++++ e2e/README.md | 3 +++ playwright.base.config.js | 34 +++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 e2e/README.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab13ff42ec..201b9ac4ac 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -189,7 +189,7 @@ jobs: if: failure() with: name: ce-playwright-trace - path: test-apps/e2e/**/test-results/**/trace.zip + path: test-apps/e2e/test-results/**/trace.zip retention-days: 1 e2e_ee: @@ -231,7 +231,7 @@ jobs: if: failure() with: name: ee-playwright-trace - path: test-apps/e2e/**/test-results/**/trace.zip + path: test-apps/e2e/test-results/**/trace.zip retention-days: 1 api_ce_pg: diff --git a/docs/docs/guides/e2e/00-setup.md b/docs/docs/guides/e2e/00-setup.md index ca77a6d7db..36e11e7862 100644 --- a/docs/docs/guides/e2e/00-setup.md +++ b/docs/docs/guides/e2e/00-setup.md @@ -29,6 +29,46 @@ This will spawn by default a Strapi instance per testing domain (e.g. content-ma If you need to clean the test-apps folder because they are not working as expected, you can run `yarn test:e2e clean` which will clean said directory. +### Running specific tests + +To run only one domain, meaning a top-level directory in e2e/tests such as "admin" or "content-manager", use the `--domains` option. + +```shell +yarn test:e2e --domains admin +yarn test:e2e --domain admin +``` + +To run a specific file, you can pass arguments and options to playwright using `--` between the test:e2e options and the playwright options, such as: + +```shell +# to run just the login.spec.ts file in the admin domain +yarn test:e2e --domains admin -- login.spec.ts +``` + +### Concurrency / parallellization + +By default, every domain is run with its own test app in parallel with the other domains. The tests within a domain are run in series, one at a time. + +If you need an easier way to view the output, or have problems running multiple apps at once on your system, you can use the `-c` option + +```shell +# only run one domain at a time +yarn test:e2e -c 1 +``` + +### Env Variables to Control Test Config + +Some helpers have been added to allow you to modify the playwright configuration on your own system without touching the playwright config file used by the test runner. + +| env var | Description | Default | +| ---------------------------- | -------------------------------------------- | ------------------ | +| PLAYWRIGHT_WEBSERVER_TIMEOUT | timeout for starting the Strapi server | 16000 (160s) | +| PLAYWRIGHT_ACTION_TIMEOUT | playwright action timeout (ie, click()) | 15000 (15s) | +| PLAYWRIGHT_EXPECT_TIMEOUT | playwright expect waitFor timeout | 10000 (10s) | +| PLAYWRIGHT_TIMEOUT | playwright timeout, for each individual test | 30000 (30s) | +| PLAYWRIGHT_OUTPUT_DIR | playwright output dir, such as trace files | '../test-results/' | +| PLAYWRIGHT_VIDEO | set 'true' to save videos on failed tests | false | + ## Strapi Templates The test-app you create uses a [template](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/templates.html) found at `e2e/app-template` in this folder we can store our premade content schemas & any customisations we may need such as other plugins / custom fields / endpoints etc. diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000000..b9f6b59e50 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,3 @@ +## End-to-end Playwright Tests + +See contributor docs in docs/docs/guides/e2e for more info diff --git a/playwright.base.config.js b/playwright.base.config.js index fa0a6f6099..4a79bb3168 100644 --- a/playwright.base.config.js +++ b/playwright.base.config.js @@ -1,5 +1,6 @@ // @ts-check const { devices } = require('@playwright/test'); +const { parseType } = require('@strapi/utils'); const getEnvNum = (envVar, defaultValue) => { if (envVar !== undefined && envVar !== null) { @@ -8,6 +9,22 @@ const getEnvNum = (envVar, defaultValue) => { return defaultValue; }; +const getEnvString = (envVar, defaultValue) => { + if (envVar?.trim().length) { + return envVar; + } + + return defaultValue; +}; + +const getEnvBool = (envVar, defaultValue) => { + if (!envVar || envVar === '') { + return defaultValue; + } + + return parseType({ type: 'boolean', value: envVar.toLowerCase() }); +}; + /** * @typedef ConfigOptions * @type {{ port: number; testDir: string; appDir: string }} @@ -28,7 +45,7 @@ const createConfig = ({ port, testDir, appDir }) => ({ * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: getEnvNum(process.env.PLAYWRIGHT_EXPECT_TIMEOUT, 30 * 1000), + timeout: getEnvNum(process.env.PLAYWRIGHT_EXPECT_TIMEOUT, 10 * 1000), }, /* Run tests in files in parallel */ fullyParallel: false, @@ -46,13 +63,22 @@ const createConfig = ({ port, testDir, appDir }) => ({ baseURL: `http://127.0.0.1:${port}`, /* Default time each action such as `click()` can take to 20s */ - actionTimeout: getEnvNum(process.env.PLAYWRIGHT_ACTION_TIMEOUT, 20 * 1000), + actionTimeout: getEnvNum(process.env.PLAYWRIGHT_ACTION_TIMEOUT, 15 * 1000), /* Collect trace when a test failed on the CI. See https://playwright.dev/docs/trace-viewer Until https://github.com/strapi/strapi/issues/18196 is fixed we can't enable this locally, because the Strapi server restarts every time a new file (trace) is created. */ - trace: process.env.CI ? 'retain-on-failure' : 'off', + trace: 'retain-on-failure', + video: getEnvBool(process.env.PLAYWRIGHT_VIDEO, false) + ? { + mode: 'retain-on-failure', // 'retain-on-failure' to save videos only for failed tests + size: { + width: 1280, + height: 720, + }, + } + : 'off', }, /* Configure projects for major browsers */ @@ -80,7 +106,7 @@ const createConfig = ({ port, testDir, appDir }) => ({ ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - outputDir: 'test-results/', + outputDir: getEnvString(process.env.PLAYWRIGHT_OUTPUT_DIR, '../test-results/'), // in the test-apps/e2e dir, to avoid writing files to the running Strapi project dir /* Run your local dev server before starting the tests */ webServer: { From 651a6b8ded3c452fedf2a8598f08c51c214e6cf7 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:34:22 +0000 Subject: [PATCH 10/14] fix(cm): fetch the init data on every mount (#19650) --- .../content-manager/hooks/useContentManagerInitData.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/content-manager/hooks/useContentManagerInitData.ts b/packages/core/admin/admin/src/content-manager/hooks/useContentManagerInitData.ts index 27bd22f4a7..a05e7c0849 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/useContentManagerInitData.ts +++ b/packages/core/admin/admin/src/content-manager/hooks/useContentManagerInitData.ts @@ -45,7 +45,13 @@ const useContentManagerInitData = (): ContentManagerAppState => { const state = useTypedSelector((state) => state['content-manager_app']); - const initialDataQuery = useGetInitialDataQuery(); + const initialDataQuery = useGetInitialDataQuery(undefined, { + /** + * TODO: remove this when the CTB has been refactored to use redux-toolkit-query + * and it can invalidate the cache on mutation + */ + refetchOnMountOrArgChange: true, + }); useEffect(() => { if (initialDataQuery.data) { From ad6d81cb6ca7fdf563554fdbed649a990b51f21c Mon Sep 17 00:00:00 2001 From: Madhuri Sandbhor Date: Mon, 4 Mar 2024 11:50:38 +0100 Subject: [PATCH 11/14] Feat(content-releases): release status badge (#19611) * feat(content-releases): add status to releases * add docs and fix e2e error * draft: added basic badge with default value * Update docs/docs/docs/01-core/content-releases/00-intro.md Co-authored-by: Simone * Update docs/docs/docs/01-core/content-releases/00-intro.md Co-authored-by: Simone * Update docs/docs/docs/01-core/content-releases/00-intro.md Co-authored-by: Simone * apply marks feedback * don't throw error on lifecycle hooks inside releases * handle when actions are not valid anymore * await for entry validation on releases edit entry * check if are changes in content types attributes to revalidate * fix e2e test * apply marks feedback * fix: removed default status value * fix: release card design updated, capitalize scheduled period * fix: e2e test updated to select always a current date --------- Co-authored-by: Fernando Chavez Co-authored-by: Simone --- .../content-releases/releases-page.spec.ts | 10 ++- .../admin/src/pages/ReleaseDetailsPage.tsx | 10 ++- .../admin/src/pages/ReleasesPage.tsx | 89 +++++++++++++------ .../pages/tests/ReleaseDetailsPage.test.tsx | 38 +++++++- .../pages/tests/mockReleaseDetailsPageData.ts | 5 +- .../src/components/RelativeTime.tsx | 4 +- 6 files changed, 123 insertions(+), 33 deletions(-) diff --git a/e2e/tests/content-releases/releases-page.spec.ts b/e2e/tests/content-releases/releases-page.spec.ts index 2f57cc6c1c..a70bdbd375 100644 --- a/e2e/tests/content-releases/releases-page.spec.ts +++ b/e2e/tests/content-releases/releases-page.spec.ts @@ -70,7 +70,15 @@ describeOnCondition(edition === 'EE')('Releases page', () => { name: 'Date', }) .click(); - await page.getByRole('gridcell', { name: 'Sunday, March 3, 2024' }).click(); + + const formattedDate = new Date().toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); + + await page.getByRole('gridcell', { name: formattedDate }).click(); await page .getByRole('combobox', { diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 4a7ca2b70f..a7805ec82e 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -60,6 +60,8 @@ import { import { useTypedDispatch } from '../store/hooks'; import { getTimezoneOffset } from '../utils/time'; +import { getBadgeProps } from './ReleasesPage'; + import type { ReleaseAction, ReleaseActionGroupBy, @@ -350,7 +352,13 @@ export const ReleaseDetailsLayout = ({ + + {numberOfEntriesText + + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : '')} + + {release.status} + } navigationAction={ } to="/plugins/content-releases"> diff --git a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx index bb0a625a05..97bf508db6 100644 --- a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useLicenseLimits } from '@strapi/admin/strapi-admin'; import { Alert, + Badge, Box, Button, ContentLayout, @@ -39,7 +40,7 @@ import { useIntl } from 'react-intl'; import { useHistory, useLocation } from 'react-router-dom'; import styled from 'styled-components'; -import { GetReleases } from '../../../shared/contracts/releases'; +import { GetReleases, type Release } from '../../../shared/contracts/releases'; import { ReleaseModal, FormValues } from '../components/ReleaseModal'; import { PERMISSIONS } from '../constants'; import { isAxiosError } from '../services/axios'; @@ -62,6 +63,37 @@ const LinkCard = styled(Link)` display: block; `; +const CapitalizeRelativeTime = styled(RelativeTime)` + text-transform: capitalize; +`; + +const getBadgeProps = (status: Release['status']) => { + let color; + switch (status) { + case 'ready': + color = 'success'; + break; + case 'blocked': + color = 'warning'; + break; + case 'failed': + color = 'danger'; + break; + case 'done': + color = 'primary'; + break; + case 'empty': + default: + color = 'neutral'; + } + + return { + textColor: `${color}600`, + backgroundColor: `${color}100`, + borderColor: `${color}200`, + }; +}; + const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: ReleasesGridProps) => { const { formatMessage } = useIntl(); const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling'); @@ -89,7 +121,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases return ( - {releases.map(({ id, name, actions, scheduledAt }) => ( + {releases.map(({ id, name, actions, scheduledAt, status }) => ( - - {name} - - - {IsSchedulingEnabled ? ( - scheduledAt ? ( - + + + {name} + + + {IsSchedulingEnabled ? ( + scheduledAt ? ( + + ) : ( + formatMessage({ + id: 'content-releases.pages.Releases.not-scheduled', + defaultMessage: 'Not scheduled', + }) + ) ) : ( - formatMessage({ - id: 'content-releases.pages.Releases.not-scheduled', - defaultMessage: 'Not scheduled', - }) - ) - ) : ( - formatMessage( - { - id: 'content-releases.page.Releases.release-item.entries', - defaultMessage: - '{number, plural, =0 {No entries} one {# entry} other {# entries}}', - }, - { number: actions.meta.count } - ) - )} - + formatMessage( + { + id: 'content-releases.page.Releases.release-item.entries', + defaultMessage: + '{number, plural, =0 {No entries} one {# entry} other {# entries}}', + }, + { number: actions.meta.count } + ) + )} + + + {status} @@ -405,4 +440,4 @@ const ReleasesPage = () => { ); }; -export { ReleasesPage }; +export { ReleasesPage, getBadgeProps }; 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 e97d905dbb..0edc7893b3 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 @@ -52,6 +52,9 @@ describe('Releases details page', () => { const releaseSubtitle = await screen.findAllByText('No entries'); expect(releaseSubtitle[0]).toBeInTheDocument(); + const releaseStatus = screen.getByText('empty'); + expect(releaseStatus).toBeInTheDocument(); + const moreButton = screen.getByRole('button', { name: 'Release edit and delete menu' }); expect(moreButton).toBeInTheDocument(); @@ -146,7 +149,7 @@ describe('Releases details page', () => { expect(tables).toHaveLength(2); }); - it('shows the right status', async () => { + it('shows the right status for unpublished release', async () => { server.use( rest.get('/content-releases/:releaseId', (req, res, ctx) => res(ctx.json(mockReleaseDetailsPageData.withActionsHeaderData)) @@ -160,7 +163,7 @@ describe('Releases details page', () => { ); render(, { - initialEntries: [{ pathname: `/content-releases/1` }], + initialEntries: [{ pathname: `/content-releases/2` }], }); const releaseTitle = await screen.findByText( @@ -168,6 +171,10 @@ describe('Releases details page', () => { ); expect(releaseTitle).toBeInTheDocument(); + const releaseStatus = screen.getByText('ready'); + expect(releaseStatus).toBeInTheDocument(); + expect(releaseStatus).toHaveStyle(`color: #328048`); + const cat1Row = screen.getByRole('row', { name: /cat1/i }); expect(within(cat1Row).getByRole('gridcell', { name: 'Ready to publish' })).toBeInTheDocument(); @@ -181,4 +188,31 @@ describe('Releases details page', () => { within(add1Row).getByRole('gridcell', { name: 'Already published' }) ).toBeInTheDocument(); }); + + it('shows the right release status for published release', async () => { + server.use( + rest.get('/content-releases/:releaseId', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withActionsAndPublishedHeaderData)) + ) + ); + + server.use( + rest.get('/content-releases/:releaseId/actions', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withMultipleActionsBodyData)) + ) + ); + + render(, { + initialEntries: [{ pathname: `/content-releases/3` }], + }); + + const releaseTitle = await screen.findByText( + mockReleaseDetailsPageData.withActionsAndPublishedHeaderData.data.name + ); + expect(releaseTitle).toBeInTheDocument(); + + const releaseStatus = screen.getByText('done'); + expect(releaseStatus).toBeInTheDocument(); + expect(releaseStatus).toHaveStyle(`color: #4945ff`); + }); }); 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 4a0c9d3bb0..7a61e30695 100644 --- a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts +++ b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts @@ -9,6 +9,7 @@ const RELEASE_NO_ACTIONS_HEADER_MOCK_DATA = { createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: null, + status: 'empty', createdBy: { id: 1, firstname: 'Admin', @@ -50,6 +51,7 @@ const RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: null, + status: 'ready', createdBy: { id: 1, firstname: 'Admin', @@ -70,11 +72,12 @@ const RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { const PUBLISHED_RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { data: { - id: 2, + id: 3, name: 'release with actions', createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: '2023-11-16T15:18:32.560Z', + status: 'done', createdBy: { id: 1, firstname: 'Admin', diff --git a/packages/core/helper-plugin/src/components/RelativeTime.tsx b/packages/core/helper-plugin/src/components/RelativeTime.tsx index 1f54585ac4..b57ecc74c8 100644 --- a/packages/core/helper-plugin/src/components/RelativeTime.tsx +++ b/packages/core/helper-plugin/src/components/RelativeTime.tsx @@ -12,6 +12,7 @@ interface CustomInterval { export interface RelativeTimeProps { timestamp: Date; customIntervals?: CustomInterval[]; + className?: string; } /** @@ -28,7 +29,7 @@ export interface RelativeTimeProps { * ]} * ``` */ -const RelativeTime = ({ timestamp, customIntervals = [] }: RelativeTimeProps) => { +const RelativeTime = ({ timestamp, customIntervals = [], className }: RelativeTimeProps) => { const { formatRelativeTime, formatDate, formatTime } = useIntl(); const interval = intervalToDuration({ @@ -54,6 +55,7 @@ const RelativeTime = ({ timestamp, customIntervals = [] }: RelativeTimeProps) => From 20c4c0d0016306a8264891064bd3f51f7dc58640 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:14:54 +0000 Subject: [PATCH 12/14] test(e2e): fix content-releases bug by advancing time by a day (#19670) --- e2e/tests/content-releases/releases-page.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/tests/content-releases/releases-page.spec.ts b/e2e/tests/content-releases/releases-page.spec.ts index a70bdbd375..95bc23b823 100644 --- a/e2e/tests/content-releases/releases-page.spec.ts +++ b/e2e/tests/content-releases/releases-page.spec.ts @@ -71,7 +71,9 @@ describeOnCondition(edition === 'EE')('Releases page', () => { }) .click(); - const formattedDate = new Date().toLocaleDateString('en-US', { + const date = new Date(); + date.setDate(date.getDate() + 1); + const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', From 48b77b64aa1e45a1d6bfa17e05c371ca35264355 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:33:15 +0000 Subject: [PATCH 13/14] test(e2e): fix transfer token tests (#19672) --- .../admin/src/pages/Settings/pages/TransferTokens/EditView.tsx | 2 +- packages/core/strapi/src/cli/commands/develop.ts | 1 + playwright.base.config.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx index df0d731f8a..34c7ab7af5 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/TransferTokens/EditView.tsx @@ -182,7 +182,7 @@ const EditView = () => { tokenType: TRANSFER_TOKEN_TYPE, }); - navigate(res.data.id.toString(), { + navigate(`../transfer-tokens/${res.data.id.toString()}`, { replace: true, state: { transferToken: res.data }, }); diff --git a/packages/core/strapi/src/cli/commands/develop.ts b/packages/core/strapi/src/cli/commands/develop.ts index a37dd01a66..66ea9db467 100644 --- a/packages/core/strapi/src/cli/commands/develop.ts +++ b/packages/core/strapi/src/cli/commands/develop.ts @@ -34,6 +34,7 @@ const command: StrapiCommand = ({ ctx }) => { .option('--ignore-prompts', 'Ignore all prompts', false) .option('--polling', 'Watch for file changes in network directories', false) .option('--watch-admin', 'Watch the admin panel for hot changes', true) + .option('--no-watch-admin', 'Do not watch the admin panel for hot changes') .option('--open', 'Open the admin in your browser', true) .description('Start your Strapi application in development mode') .action(async (options: DevelopCLIOptions) => { diff --git a/playwright.base.config.js b/playwright.base.config.js index e26458ce31..0e98dd3056 100644 --- a/playwright.base.config.js +++ b/playwright.base.config.js @@ -110,7 +110,7 @@ const createConfig = ({ port, testDir, appDir }) => ({ /* Run your local dev server before starting the tests */ webServer: { - command: `cd ${appDir} && npm run develop`, + command: `cd ${appDir} && npm run develop -- --no-watch-admin`, url: `http://127.0.0.1:${port}`, /* default Strapi server startup timeout to 160s */ timeout: getEnvNum(process.env.PLAYWRIGHT_WEBSERVER_TIMEOUT, 160 * 1000), From 5153a18c04f5f229483d31dd9482f37a7bc0836b Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 4 Mar 2024 12:27:34 +0100 Subject: [PATCH 14/14] chore: merge repo & doc-engine into one before cleaning up --- .../src/services/document-service/common.ts | 4 +- .../document-service/document-engine.ts | 345 --------------- .../src/services/document-service/index.ts | 2 +- .../repositories/content-type.ts | 172 -------- .../services/document-service/repository.ts | 411 ++++++++++++++++++ .../src/modules/documents/document-engine.ts | 108 ----- .../core/types/src/modules/documents/index.ts | 3 +- .../src/modules/documents/service-instance.ts | 30 +- .../core/utils/src/convert-query-params.ts | 7 +- 9 files changed, 423 insertions(+), 659 deletions(-) delete mode 100644 packages/core/core/src/services/document-service/document-engine.ts delete mode 100644 packages/core/core/src/services/document-service/repositories/content-type.ts create mode 100644 packages/core/core/src/services/document-service/repository.ts delete mode 100644 packages/core/types/src/modules/documents/document-engine.ts diff --git a/packages/core/core/src/services/document-service/common.ts b/packages/core/core/src/services/document-service/common.ts index eb1d584d7f..a09bd0d296 100644 --- a/packages/core/core/src/services/document-service/common.ts +++ b/packages/core/core/src/services/document-service/common.ts @@ -1,6 +1,6 @@ -import type { Common } from '@strapi/types'; +import type { Common, Documents } from '@strapi/types'; -export type RepositoryFactoryMethod = (uid: Common.UID.CollectionType) => any; +export type RepositoryFactoryMethod = (uid: Common.UID.CollectionType) => Documents.ServiceInstance; export const wrapInTransaction = (fn: (...args: any) => any) => { return (...args: any[]) => strapi.db.transaction?.(() => fn(...args)); diff --git a/packages/core/core/src/services/document-service/document-engine.ts b/packages/core/core/src/services/document-service/document-engine.ts deleted file mode 100644 index 112a4ab0f5..0000000000 --- a/packages/core/core/src/services/document-service/document-engine.ts +++ /dev/null @@ -1,345 +0,0 @@ -import type { Database } from '@strapi/database'; -import type { Documents, Schema, Strapi } from '@strapi/types'; -import { - contentTypes as contentTypesUtils, - convertQueryParams, - mapAsync, - pipeAsync, -} from '@strapi/utils'; - -import { omit, set } from 'lodash/fp'; - -import { - cloneComponents, - createComponents, - deleteComponents, - getComponents, - omitComponentData, - updateComponents, -} from '../entity-service/components'; - -import { createDocumentId } from '../../utils/transform-content-types-to-models'; -import { applyTransforms } from '../entity-service/attributes'; -import entityValidator from '../entity-validator'; -import { pickSelectionParams } from './params'; -import { transformParamsDocumentId } from './transform/id-transform'; -import { getDeepPopulate } from './utils/populate'; -import { transformData } from './transform/data'; - -const { transformParamsToQuery } = convertQueryParams; - -/** - * TODO: Sanitization / validation built-in - * TODO: i18n - Move logic to i18n package - * TODO: Webhooks - * TODO: Audit logs - * TODO: replace 'any' - * TODO: availableLocales - * - */ -type Context = { - contentType: Schema.ContentType; -}; - -const createPipeline = (data: Record, context: Context) => { - return applyTransforms(data, context); -}; - -const updatePipeline = (data: Record, context: Context) => { - return applyTransforms(data, context); -}; - -const createDocumentEngine = ({ - strapi, - db, -}: { - strapi: Strapi; - db: Database; -}): Documents.Engine => ({ - async findMany(uid, params) { - const query = await pipeAsync( - (params) => transformParamsDocumentId(uid, params, { isDraft: params.status === 'draft' }), - (params) => transformParamsToQuery(uid, params), - (query) => set('where', { ...params?.lookup, ...query.where }, query) - )(params || {}); - - return db.query(uid).findMany(query); - }, - - async findFirst(uid, params) { - const query = await pipeAsync( - (params) => transformParamsDocumentId(uid, params, { isDraft: params.status === 'draft' }), - (params) => transformParamsToQuery(uid, params) - )(params || {}); - - return db.query(uid).findOne({ ...query, where: { ...params?.lookup, ...query.where } }); - }, - - async findOne(uid, documentId, params) { - const query = await pipeAsync( - (params) => transformParamsDocumentId(uid, params, { isDraft: params.status === 'draft' }), - (params) => transformParamsToQuery(uid, params) - )(params || {}); - - return db - .query(uid) - .findOne({ ...query, where: { ...params?.lookup, ...query.where, documentId } }); - }, - - async delete(uid, documentId, params = {} as any) { - const query = await pipeAsync( - (params) => transformParamsToQuery(uid, params), - (query) => set('where', { ...params?.lookup, ...query.where, documentId }, query) - )(params); - - if (params.status === 'draft') { - throw new Error('Cannot delete a draft document'); - } - - const entriesToDelete = await db.query(uid).findMany(query); - - // Delete all matched entries and its components - await mapAsync(entriesToDelete, async (entryToDelete: any) => { - await this.deleteEntry(uid, entryToDelete.id); - }); - - return { deletedEntries: entriesToDelete.length }; - }, - - async deleteEntry(uid, entryId) { - const componentsToDelete = await getComponents(uid, { id: entryId }); - - await db.query(uid).delete({ where: { id: entryId } }); - - await deleteComponents(uid, componentsToDelete as any, { loadComponents: false }); - }, - - async create(uid, params) { - // Param parsing - const { data, ...restParams } = await transformParamsDocumentId(uid, params, { - locale: params.locale, - // @ts-expect-error - published at is not always present - // User can not set publishedAt on create, but other methods in the engine can (publish) - isDraft: !params.data?.publishedAt, - }); - - const query = transformParamsToQuery(uid, pickSelectionParams(restParams) as any); // select / populate - - // Validation - if (!params.data) { - throw new Error('Create requires data attribute'); - } - - const contentType = strapi.contentType(uid); - - const validData = await entityValidator.validateEntityCreation(contentType, data, { - isDraft: !data.publishedAt, - locale: params?.locale, - }); - - // Component handling - const componentData = await createComponents(uid, validData as any); - const entryData = createPipeline( - Object.assign(omitComponentData(contentType, validData), componentData), - { contentType } - ); - - return db.query(uid).create({ ...query, data: entryData }); - }, - - // NOTE: What happens if user doesn't provide specific publications state and locale to update? - async update(uid, documentId, params) { - // Param parsing - const { data, ...restParams } = await transformParamsDocumentId(uid, params || {}, { - isDraft: true, - locale: params?.locale, - }); - const query = transformParamsToQuery(uid, pickSelectionParams(restParams || {}) as any); - - // Validation - const model = strapi.contentType(uid); - // Find if document exists - const entryToUpdate = await db - .query(uid) - .findOne({ ...query, where: { ...params?.lookup, ...query?.where, documentId } }); - if (!entryToUpdate) return null; - - const validData = await entityValidator.validateEntityUpdate( - model, - data, - { - isDraft: true, // Always update the draft version - locale: params?.locale, - }, - entryToUpdate - ); - - // Component handling - const componentData = await updateComponents(uid, entryToUpdate, validData as any); - const entryData = updatePipeline( - Object.assign(omitComponentData(model, validData), componentData), - { contentType: model } - ); - - return db.query(uid).update({ ...query, where: { id: entryToUpdate.id }, data: entryData }); - }, - - async count(uid, params = undefined) { - const query = await pipeAsync( - (params) => transformParamsToQuery(uid, params), - (query) => set('where', { ...params?.lookup, ...query.where }, query) - )(params || {}); - - return db.query(uid).count(query); - }, - - async clone(uid, documentId, params) { - const { data, ...restParams } = await transformParamsDocumentId(uid, params || {}, { - isDraft: true, - locale: params?.locale, - }); - const query = transformParamsToQuery(uid, pickSelectionParams(restParams) as any); - // Param parsing - - // Validation - const model = strapi.contentType(uid); - // Find all locales of the document - const entries = await db.query(uid).findMany({ - ...query, - where: { ...params?.lookup, ...query.where, documentId }, - }); - - // Document does not exist - if (!entries.length) { - return null; - } - - const newDocumentId = createDocumentId(); - - const versions = await mapAsync(entries, async (entryToClone: any) => { - const isDraft = contentTypesUtils.isDraft(data); - // Todo: Merge data with entry to clone - const validData = await entityValidator.validateEntityUpdate( - model, - // Omit id fields, the cloned entity id will be generated by the database - omit(['id'], data), - { isDraft, ...params?.lookup }, - entryToClone - ); - - const componentData = await cloneComponents(uid, entryToClone, validData); - const entityData = createPipeline( - Object.assign(omitComponentData(model, validData), componentData), - { contentType: model } - ); - - // TODO: Transform params to query - return db.query(uid).clone(entryToClone.id, { - ...query, - // Allows entityData to override the documentId (e.g. when publishing) - data: { documentId: newDocumentId, ...entityData, locale: entryToClone.locale }, - }); - }); - - return { documentId: newDocumentId, versions }; - }, - - // TODO: Handle relations so they target the published version - async publish(uid, documentId, params) { - // Delete already published versions that match the locales to be published - await this.delete(uid, documentId, { - ...params, - lookup: { ...params?.lookup, publishedAt: { $ne: null } }, - }); - - // Get deep populate - const entriesToPublish = await strapi.db?.query(uid).findMany({ - where: { - ...params?.lookup, - documentId, - publishedAt: null, - }, - populate: getDeepPopulate(uid), - }); - - // Transform draft entry data and create published versions - const publishedEntries = await mapAsync( - entriesToPublish, - pipeAsync( - set('publishedAt', new Date()), - set('documentId', documentId), - omit('id'), - // Transform relations to target published versions - (entry) => { - const opts = { uid, locale: entry.locale, isDraft: false, allowMissingId: true }; - return transformData(entry, opts); - }, - // Create the published entry - (data) => this.create(uid, { ...params, data, locale: data.locale }) - ) - ); - - return { versions: publishedEntries }; - }, - - async unpublish(uid, documentId, params) { - // Delete all published versions - return this.delete(uid, documentId, { - ...params, - lookup: { ...params?.lookup, publishedAt: { $ne: null } }, - }).then(({ deletedEntries }) => ({ versions: deletedEntries })) as any; - }, - - /** - * Steps: - * - Delete the matching draft versions (publishedAt = null) - * - Clone the matching published versions into draft versions - * - * If the document has a published version, the draft version will be created from the published version. - * If the document has no published version, the version will be removed. - */ - async discardDraft(uid, documentId, params) { - // Delete draft versions, clone published versions into draft versions - await this.delete(uid, documentId, { - ...params, - // Delete all drafts that match query - lookup: { ...params?.lookup, publishedAt: null }, - }); - - // Get deep populate of published versions - const entriesToDraft = await strapi.db?.query(uid).findMany({ - where: { - ...params?.lookup, - documentId, - publishedAt: { $ne: null }, - }, - populate: getDeepPopulate(uid), - }); - - // Transform published entry data and create draft versions - const draftEntries = await mapAsync( - entriesToDraft, - pipeAsync( - set('publishedAt', null), - set('documentId', documentId), - omit('id'), - // Transform relations to target draft versions - (entry) => { - const opts = { uid, locale: entry.locale, isDraft: true, allowMissingId: true }; - return transformData(entry, opts); - }, - // Create the draft entry - (data) => this.create(uid, { ...params, locale: data.locale, data }) - ) - ); - - return { versions: draftEntries }; - }, -}); - -export default (ctx: { strapi: Strapi; db: Database }): Documents.Engine => { - const implementation = createDocumentEngine(ctx); - - // TODO: Wrap with database error handling - return implementation; -}; diff --git a/packages/core/core/src/services/document-service/index.ts b/packages/core/core/src/services/document-service/index.ts index 1491a9791c..d41b658801 100644 --- a/packages/core/core/src/services/document-service/index.ts +++ b/packages/core/core/src/services/document-service/index.ts @@ -1,7 +1,7 @@ import { Strapi, Documents } from '@strapi/types'; import { createMiddlewareManager } from './middlewares'; -import { createContentTypeRepository } from './repositories/content-type'; +import { createContentTypeRepository } from './repository'; /** * Repository to : diff --git a/packages/core/core/src/services/document-service/repositories/content-type.ts b/packages/core/core/src/services/document-service/repositories/content-type.ts deleted file mode 100644 index 1959db8150..0000000000 --- a/packages/core/core/src/services/document-service/repositories/content-type.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { omit } from 'lodash/fp'; - -import { pipeAsync } from '@strapi/utils'; - -import { wrapInTransaction, type RepositoryFactoryMethod } from '../common'; -import createDocumentEngine from '../document-engine'; -import * as DP from '../draft-and-publish'; -import * as i18n from '../internationalization'; - -export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => { - const contentType = strapi.contentType(uid); - - // TODO: move the code back into here instead of using the document-engine - const documents = createDocumentEngine({ strapi, db: strapi?.db }); - - async function findMany(params = {} as any) { - // TODO: replace with chaining - const queryParams = await pipeAsync( - DP.defaultToDraft, - DP.statusToLookup, - i18n.defaultLocale(contentType), - i18n.localeToLookup(contentType) - )(params); - - return documents.findMany(uid, queryParams); - } - - async function findFirst(params = {} as any) { - const queryParams = await pipeAsync( - DP.defaultToDraft, - DP.statusToLookup, - i18n.defaultLocale(contentType), - i18n.localeToLookup(contentType) - )(params); - - return documents.findFirst(uid, queryParams); - } - - async function findOne(id: string, params = {} as any) { - const queryParams = await pipeAsync( - DP.defaultToDraft, - DP.statusToLookup, - i18n.defaultLocale(contentType), - i18n.localeToLookup(contentType) - )(params); - - return documents.findOne(uid, id, queryParams); - } - - async function deleteFn(id: string, params = {} as any) { - const queryParams = await pipeAsync( - omit('status'), - i18n.defaultLocale(contentType), - i18n.multiLocaleToLookup(contentType) - )(params); - - return documents.delete(uid, id, queryParams); - } - - async function create(params = {} as any) { - const queryParams = await pipeAsync( - DP.setStatusToDraft, - DP.statusToData, - DP.filterDataPublishedAt, - i18n.defaultLocale(contentType), - i18n.localeToData(contentType) - )(params); - - const doc = await documents.create(uid, queryParams); - - if (params.status === 'published') { - return publish(doc.documentId, params).then((doc) => doc.versions[0]); - } - - return doc; - } - - async function clone(id: string, params = {} as any) { - const queryParams = await pipeAsync( - DP.filterDataPublishedAt, - i18n.localeToLookup(contentType) - )(params); - - return documents.clone(uid, id, queryParams); - } - - async function update(id: string, params = {} as any) { - const queryParams = await pipeAsync( - DP.setStatusToDraft, - DP.statusToLookup, - DP.statusToData, - DP.filterDataPublishedAt, - // Default locale will be set if not provided - i18n.defaultLocale(contentType), - i18n.localeToLookup(contentType), - i18n.localeToData(contentType) - )(params); - - let updatedDraft: any = await documents.update(uid, id, queryParams); - - if (!updatedDraft) { - const documentExists = await strapi.db - .query(contentType.uid) - .findOne({ where: { documentId: id } }); - - if (documentExists) { - updatedDraft = await create({ - ...queryParams, - data: { ...queryParams.data, documentId: id }, - }); - } - } - - if (updatedDraft && params.status === 'published') { - return publish(id, params).then((doc) => doc.versions[0]); - } - - return updatedDraft; - } - - async function count(params = {} as any) { - const queryParams = await pipeAsync( - DP.defaultToDraft, - DP.statusToLookup, - i18n.defaultLocale(contentType), - i18n.localeToLookup(contentType) - )(params); - - return documents.count(uid, queryParams); - } - - async function publish(id: string, params = {} as any) { - const queryParams = await pipeAsync( - i18n.defaultLocale(contentType), - i18n.multiLocaleToLookup(contentType) - )(params); - - return documents.publish(uid, id, queryParams); - } - - async function unpublish(id: string, params = {} as any) { - const queryParams = await pipeAsync( - i18n.defaultLocale(contentType), - i18n.multiLocaleToLookup(contentType) - )(params); - - return documents.unpublish(uid, id, queryParams); - } - - async function discardDraft(id: string, params = {} as any) { - const queryParams = await pipeAsync( - i18n.defaultLocale(contentType), - i18n.multiLocaleToLookup(contentType) - )(params); - - return documents.discardDraft(uid, id, queryParams); - } - - return { - findMany: wrapInTransaction(findMany), - findFirst: wrapInTransaction(findFirst), - findOne: wrapInTransaction(findOne), - delete: wrapInTransaction(deleteFn), - create: wrapInTransaction(create), - clone: wrapInTransaction(clone), - update: wrapInTransaction(update), - count: wrapInTransaction(count), - publish: wrapInTransaction(publish), - unpublish: wrapInTransaction(unpublish), - discardDraft: wrapInTransaction(discardDraft), - }; -}; diff --git a/packages/core/core/src/services/document-service/repository.ts b/packages/core/core/src/services/document-service/repository.ts new file mode 100644 index 0000000000..a0b97ecc67 --- /dev/null +++ b/packages/core/core/src/services/document-service/repository.ts @@ -0,0 +1,411 @@ +import { omit, assoc, curry } from 'lodash/fp'; + +import { + pipeAsync, + mapAsync, + convertQueryParams, + contentTypes as contentTypesUtils, +} from '@strapi/utils'; +import { Common } from '@strapi/types'; + +import { wrapInTransaction, type RepositoryFactoryMethod } from './common'; +import * as DP from './draft-and-publish'; +import * as i18n from './internationalization'; +import { transformParamsDocumentId } from './transform/id-transform'; + +import { + cloneComponents, + createComponents, + deleteComponents, + getComponents, + omitComponentData, + updateComponents, +} from '../entity-service/components'; + +import { pickSelectionParams } from './params'; +import entityValidator from '../entity-validator'; +import { applyTransforms } from '../entity-service/attributes'; +import { createDocumentId } from '../../utils/transform-content-types-to-models'; +import { getDeepPopulate } from './utils/populate'; +import { transformData } from './transform/data'; + +const transformParamsToQuery = curry((uid: Common.UID.Schema, params: any) => { + const query = convertQueryParams.transformParamsToQuery(uid, params); + + return assoc('where', { ...params?.lookup, ...query.where }, query); +}); + +export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => { + const contentType = strapi.contentType(uid); + + async function findMany(params = {} as any) { + const query = await pipeAsync( + DP.defaultToDraft, + DP.statusToLookup, + i18n.defaultLocale(contentType), + i18n.localeToLookup(contentType), + (queryParams) => + transformParamsDocumentId(uid, queryParams, { isDraft: queryParams.status === 'draft' }), + transformParamsToQuery(uid) + )(params || {}); + + return strapi.db.query(uid).findMany(query); + } + + async function findFirst(params = {} as any) { + const query = await pipeAsync( + DP.defaultToDraft, + DP.statusToLookup, + i18n.defaultLocale(contentType), + i18n.localeToLookup(contentType), + (queryParams) => + transformParamsDocumentId(uid, queryParams, { isDraft: queryParams.status === 'draft' }), + transformParamsToQuery(uid) + )(params); + + return strapi.db.query(uid).findOne(query); + } + + // TODO: do we really want to add filters on the findOne now that we have findFirst ? + async function findOne(documentId: string, params = {} as any) { + const query = await pipeAsync( + DP.defaultToDraft, + DP.statusToLookup, + i18n.defaultLocale(contentType), + i18n.localeToLookup(contentType), + (queryParams) => + transformParamsDocumentId(uid, queryParams, { isDraft: queryParams.status === 'draft' }), + transformParamsToQuery(uid), + (query) => assoc('where', { ...query.where, documentId }, query) + )(params); + + return strapi.db.query(uid).findOne(query); + } + + async function deleteEntry(id: number) { + const componentsToDelete = await getComponents(uid, { id }); + + await strapi.db.query(uid).delete({ where: { id } }); + + await deleteComponents(uid, componentsToDelete as any, { loadComponents: false }); + } + + async function deleteFn(documentId: string, params = {} as any) { + const query = await pipeAsync( + omit('status'), + i18n.defaultLocale(contentType), + i18n.multiLocaleToLookup(contentType), + transformParamsToQuery(uid), + (query) => assoc('where', { ...query.where, documentId }, query) + )(params); + + if (params.status === 'draft') { + throw new Error('Cannot delete a draft document'); + } + + const entriesToDelete = await strapi.db.query(uid).findMany(query); + + // Delete all matched entries and its components + await mapAsync(entriesToDelete, (entryToDelete: any) => deleteEntry(entryToDelete.id)); + + return { deletedEntries: entriesToDelete.length }; + } + + async function createEntry(params = {} as any) { + const { data, ...restParams } = await transformParamsDocumentId(uid, params, { + locale: params.locale, + // User can not set publishedAt on create, but other methods in the engine can (publish) + isDraft: !params.data?.publishedAt, + }); + + const query = transformParamsToQuery(uid, pickSelectionParams(restParams) as any); // select / populate + + // Validation + if (!params.data) { + throw new Error('Create requires data attribute'); + } + + const validData = await entityValidator.validateEntityCreation(contentType, data, { + isDraft: !data.publishedAt, + locale: params?.locale, + }); + + // Component handling + const componentData = await createComponents(uid, validData as any); + const entryData = applyTransforms( + Object.assign(omitComponentData(contentType, validData), componentData), + { contentType } + ); + + const doc = await strapi.db.query(uid).create({ ...query, data: entryData }); + + return doc; + } + + async function create(params = {} as any) { + const queryParams = await pipeAsync( + DP.setStatusToDraft, + DP.statusToData, + DP.filterDataPublishedAt, + i18n.defaultLocale(contentType), + i18n.localeToData(contentType) + )(params); + + const doc = await createEntry(queryParams); + + if (params.status === 'published') { + return publish(doc.documentId, params).then((doc) => doc.versions[0]); + } + + return doc; + } + + async function clone(documentId: string, params = {} as any) { + const queryParams = await pipeAsync( + DP.filterDataPublishedAt, + i18n.localeToLookup(contentType) + )(params); + + const { data, ...restParams } = await transformParamsDocumentId(uid, queryParams || {}, { + isDraft: true, + locale: queryParams?.locale, + }); + const query = transformParamsToQuery(uid, pickSelectionParams(restParams) as any); + // Param parsing + + // Validation + const model = strapi.contentType(uid); + // Find all locales of the document + const entries = await strapi.db.query(uid).findMany({ + ...query, + where: { ...queryParams?.lookup, ...query.where, documentId }, + }); + + // Document does not exist + if (!entries.length) { + return null; + } + + const newDocumentId = createDocumentId(); + + const versions = await mapAsync(entries, async (entryToClone: any) => { + const isDraft = contentTypesUtils.isDraft(data); + // Todo: Merge data with entry to clone + const validData = await entityValidator.validateEntityUpdate( + model, + // Omit id fields, the cloned entity id will be generated by the database + omit(['id'], data), + { isDraft, ...queryParams?.lookup }, + entryToClone + ); + + const componentData = await cloneComponents(uid, entryToClone, validData); + const entityData = applyTransforms( + Object.assign(omitComponentData(model, validData), componentData), + { contentType: model } + ); + + // TODO: Transform params to query + return strapi.db.query(uid).clone(entryToClone.id, { + ...query, + // Allows entityData to override the documentId (e.g. when publishing) + data: { documentId: newDocumentId, ...entityData, locale: entryToClone.locale }, + }); + }); + + return { documentId: newDocumentId, versions }; + } + + // NOTE: What happens if user doesn't provide specific publications state and locale to update? + async function update(documentId: string, params = {} as any) { + const queryParams = await pipeAsync( + DP.setStatusToDraft, + DP.statusToLookup, + DP.statusToData, + DP.filterDataPublishedAt, + // Default locale will be set if not provided + i18n.defaultLocale(contentType), + i18n.localeToLookup(contentType), + i18n.localeToData(contentType) + )(params); + + const { data, ...restParams } = await transformParamsDocumentId(uid, queryParams || {}, { + isDraft: true, + locale: queryParams?.locale, + }); + const query = transformParamsToQuery(uid, pickSelectionParams(restParams || {}) as any); + + // Validation + const model = strapi.contentType(uid); + // Find if document exists + const entryToUpdate = await strapi.db + .query(uid) + .findOne({ ...query, where: { ...queryParams?.lookup, ...query?.where, documentId } }); + + let updatedDraft = null; + if (entryToUpdate) { + const validData = await entityValidator.validateEntityUpdate( + model, + data, + { + isDraft: true, // Always update the draft version + locale: queryParams?.locale, + }, + entryToUpdate + ); + + // Component handling + const componentData = await updateComponents(uid, entryToUpdate, validData as any); + const entryData = applyTransforms( + Object.assign(omitComponentData(model, validData), componentData), + { contentType: model } + ); + + updatedDraft = await strapi.db + .query(uid) + .update({ ...query, where: { id: entryToUpdate.id }, data: entryData }); + } + + if (!updatedDraft) { + const documentExists = await strapi.db + .query(contentType.uid) + .findOne({ where: { documentId } }); + + if (documentExists) { + updatedDraft = await createEntry({ + ...queryParams, + data: { ...queryParams.data, documentId }, + }); + } + } + + if (updatedDraft && params.status === 'published') { + return publish(documentId, params).then((doc) => doc.versions[0]); + } + + return updatedDraft; + } + + async function count(params = {} as any) { + const query = await pipeAsync( + DP.defaultToDraft, + DP.statusToLookup, + i18n.defaultLocale(contentType), + i18n.localeToLookup(contentType), + transformParamsToQuery(uid) + )(params); + + return strapi.db.query(uid).count(query); + } + + async function publish(documentId: string, params = {} as any) { + const queryParams = await pipeAsync( + i18n.defaultLocale(contentType), + i18n.multiLocaleToLookup(contentType) + )(params); + + await deleteFn(documentId, { + ...queryParams, + lookup: { ...queryParams?.lookup, publishedAt: { $ne: null } }, + }); + + // Get deep populate + const entriesToPublish = await strapi.db?.query(uid).findMany({ + where: { + ...queryParams?.lookup, + documentId, + publishedAt: null, + }, + populate: getDeepPopulate(uid), + }); + + // Transform draft entry data and create published versions + const publishedEntries = await mapAsync( + entriesToPublish, + pipeAsync( + assoc('publishedAt', new Date()), + assoc('documentId', documentId), + omit('id'), + // Transform relations to target published versions + (entry) => { + const opts = { uid, locale: entry.locale, isDraft: false, allowMissingId: true }; + return transformData(entry, opts); + }, + // Create the published entry + (data) => createEntry({ ...queryParams, data, locale: data.locale }) + ) + ); + + return { versions: publishedEntries }; + } + + async function unpublish(documentId: string, params = {} as any) { + const queryParams = await pipeAsync( + i18n.defaultLocale(contentType), + i18n.multiLocaleToLookup(contentType) + )(params); + + const { deletedEntries } = await deleteFn(documentId, { + ...params, + lookup: { ...queryParams?.lookup, publishedAt: { $ne: null } }, + }); + + return { versions: deletedEntries }; + } + + async function discardDraft(documentId: string, params = {} as any) { + const queryParams = await pipeAsync( + i18n.defaultLocale(contentType), + i18n.multiLocaleToLookup(contentType) + )(params); + + await deleteFn(documentId, { + ...queryParams, + // Delete all drafts that match query + lookup: { ...queryParams?.lookup, publishedAt: null }, + }); + + // Get deep populate of published versions + const entriesToDraft = await strapi.db?.query(uid).findMany({ + where: { + ...queryParams?.lookup, + documentId, + publishedAt: { $ne: null }, + }, + populate: getDeepPopulate(uid), + }); + + // Transform published entry data and create draft versions + const draftEntries = await mapAsync( + entriesToDraft, + pipeAsync( + assoc('publishedAt', null), + assoc('documentId', documentId), + omit('id'), + // Transform relations to target draft versions + (entry) => { + const opts = { uid, locale: entry.locale, isDraft: true, allowMissingId: true }; + return transformData(entry, opts); + }, + // Create the draft entry + (data) => createEntry({ ...queryParams, locale: data.locale, data }) + ) + ); + + return { versions: draftEntries }; + } + + return { + findMany: wrapInTransaction(findMany), + findFirst: wrapInTransaction(findFirst), + findOne: wrapInTransaction(findOne), + delete: wrapInTransaction(deleteFn), + create: wrapInTransaction(create), + clone: wrapInTransaction(clone), + update: wrapInTransaction(update), + count: wrapInTransaction(count), + publish: wrapInTransaction(publish), + unpublish: wrapInTransaction(unpublish), + discardDraft: wrapInTransaction(discardDraft), + }; +}; diff --git a/packages/core/types/src/modules/documents/document-engine.ts b/packages/core/types/src/modules/documents/document-engine.ts deleted file mode 100644 index c051c3ecae..0000000000 --- a/packages/core/types/src/modules/documents/document-engine.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Common } from '../../types'; -import type * as Params from './params/document-engine'; -import type * as Result from './result/document-enigne'; - -export type ID = string; - -export interface DocumentEngine { - findMany< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.FindMany - >( - uid: TContentTypeUID, - params?: TParams - ): Result.FindMany; - - findFirst< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.FindFirst - >( - uid: TContentTypeUID, - params?: TParams - ): Result.FindFirst; - - findOne< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.FindOne - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.FindOne; - - delete< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Delete - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.Delete; - - create< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Create - >( - uid: TContentTypeUID, - params: TParams - ): Result.Create; - - clone< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Clone - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.Clone; - - update< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Update - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.Update; - - count< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Count - >( - uid: TContentTypeUID, - params?: TParams - ): Result.Count; - - publish< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Publish - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.Publish; - - unpublish< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.Unpublish - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.Unpublish; - - discardDraft< - TContentTypeUID extends Common.UID.ContentType, - TParams extends Params.DiscardDraft - >( - uid: TContentTypeUID, - documentId: ID, - params?: TParams - ): Result.DiscardDraft; - - // Entry utilities - deleteEntry( - uid: TContentTypeUID, - entryId: number | string - ): Promise; -} diff --git a/packages/core/types/src/modules/documents/index.ts b/packages/core/types/src/modules/documents/index.ts index c42798feec..f0dfaf2cbb 100644 --- a/packages/core/types/src/modules/documents/index.ts +++ b/packages/core/types/src/modules/documents/index.ts @@ -2,13 +2,14 @@ import type { Schema } from '../..'; import type * as Middleware from './middleware'; import type { ServiceInstance } from './service-instance'; -export { ID, DocumentEngine as Engine } from './document-engine'; export * as Middleware from './middleware'; export * as Params from './params'; export * from './plugin'; export * from './result'; export * from './service-instance'; +export type ID = string; + export type Service = { (uid: Schema.ContentType['uid']): ServiceInstance; diff --git a/packages/core/types/src/modules/documents/service-instance.ts b/packages/core/types/src/modules/documents/service-instance.ts index d19f3836c3..2112d6452d 100644 --- a/packages/core/types/src/modules/documents/service-instance.ts +++ b/packages/core/types/src/modules/documents/service-instance.ts @@ -1,5 +1,5 @@ -import type { Common, Documents } from '../..'; -import type { ID } from './document-engine'; +import type { Common } from '../..'; +import type { ID } from '.'; import type * as Params from './params/document-engine'; import type * as Result from './result/document-enigne'; @@ -55,29 +55,3 @@ export type ServiceInstance< params?: TParams ) => Result.DiscardDraft; }; - -export type SingleTypeInstance< - TContentTypeUID extends Common.UID.ContentType = Common.UID.ContentType -> = { - find: >( - params?: TParams - ) => Promise>; - - delete: >(params?: TParams) => Result.Delete; - - update: >( - params: TParams - ) => Promise>; - - publish: >( - params?: TParams - ) => Promise>; - - unpublish: >( - params?: TParams - ) => Promise>; - - discardDraft: >( - params?: TParams - ) => Promise>; -}; diff --git a/packages/core/utils/src/convert-query-params.ts b/packages/core/utils/src/convert-query-params.ts index dbef150bb4..71202918fa 100644 --- a/packages/core/utils/src/convert-query-params.ts +++ b/packages/core/utils/src/convert-query-params.ts @@ -610,7 +610,7 @@ const transformParamsToQuery = (uid: string, params: Params): Query => { const query: Query = {}; - const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params; + const { _q, sort, filters, fields, populate, page, pageSize, start, limit, ...rest } = params; if (!isNil(_q)) { query._q = _q; @@ -650,7 +650,10 @@ const transformParamsToQuery = (uid: string, params: Params): Query => { query.limit = convertLimitQueryParams(limit); } - return query; + return { + ...rest, + ...query, + }; }; export default {