From 6529e2bd503523543d2b6d1004398c06d75cc00d Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 24 Jun 2024 19:49:08 +0200 Subject: [PATCH 1/3] fix: several UI issues a typos --- .../pages/Webhooks/components/Events.tsx | 16 +++++---- .../Webhooks/components/HeadersInput.tsx | 35 +++++++++++-------- .../SettingsPage/pages/AuditLogs/ListPage.tsx | 15 ++------ .../Permissions/PermissionRow/SubCategory.jsx | 4 +-- .../admin/src/pages/Providers/index.jsx | 2 +- .../pages/ListPage/components/TableBody.jsx | 2 +- 6 files changed, 36 insertions(+), 38 deletions(-) diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/Events.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/Events.tsx index 739a588be5..2c62b68fd4 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/Events.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/Events.tsx @@ -286,13 +286,15 @@ const EventsRow = ({ {events.map((event) => { return ( - handleSelect(event, !!value)} - /> + + handleSelect(event, !!value)} + /> + ); })} diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/HeadersInput.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/HeadersInput.tsx index 9fe671622b..caa31ffeaa 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/HeadersInput.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/HeadersInput.tsx @@ -13,10 +13,15 @@ import { } from '@strapi/design-system'; import { Minus, Plus } from '@strapi/icons'; import { useIntl } from 'react-intl'; +import { styled } from 'styled-components'; import { useField, useForm } from '../../../../../components/Form'; import { StringInput } from '../../../../../components/FormInputs/String'; +const AddHeaderButton = styled(TextButton)` + cursor: pointer; +`; + /* ------------------------------------------------------------------------------------------------- * HeadersInput * -----------------------------------------------------------------------------------------------*/ @@ -44,7 +49,7 @@ const HeadersInput = () => { {value.map((_, index) => { return ( - + { - - { - addFieldRow('headers', { key: '', value: '' }); - }} - startIcon={} - > - {formatMessage({ - id: 'Settings.webhooks.create.header', - defaultMessage: 'Create new header', - })} - - ); })} + + { + addFieldRow('headers', { key: '', value: '' }); + }} + startIcon={} + > + {formatMessage({ + id: 'Settings.webhooks.create.header', + defaultMessage: 'Create new header', + })} + + ); diff --git a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/ListPage.tsx b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/ListPage.tsx index 3383bbbf6b..df0bea8815 100644 --- a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/ListPage.tsx +++ b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/ListPage.tsx @@ -72,14 +72,6 @@ const ListPage = () => { // In this case, the passed parameter cannot and shouldn't be something else than User cellFormatter: ({ user }) => (user ? user.displayName : ''), }, - { - name: 'actions', - label: formatMessage({ - id: 'Settings.permissions.auditLogs.actions', - defaultMessage: 'Actions', - }), - sortable: false, - }, ]; if (hasError) { @@ -170,8 +162,6 @@ const ListPage = () => { ); - case 'actions': - return null; default: return ( @@ -202,8 +192,9 @@ const ListPage = () => { - - + + + diff --git a/packages/plugins/users-permissions/admin/src/components/Permissions/PermissionRow/SubCategory.jsx b/packages/plugins/users-permissions/admin/src/components/Permissions/PermissionRow/SubCategory.jsx index 083995a41a..4de86c7d95 100644 --- a/packages/plugins/users-permissions/admin/src/components/Permissions/PermissionRow/SubCategory.jsx +++ b/packages/plugins/users-permissions/admin/src/components/Permissions/PermissionRow/SubCategory.jsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo } from 'react'; import { Box, Checkbox, Flex, Typography, Grid, VisuallyHidden } from '@strapi/design-system'; -import { Cog as CogIcon } from '@strapi/icons'; +import { Cog } from '@strapi/icons'; import get from 'lodash/get'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; @@ -103,7 +103,7 @@ const SubCategory = ({ subCategory }) => { } )} - + diff --git a/packages/plugins/users-permissions/admin/src/pages/Providers/index.jsx b/packages/plugins/users-permissions/admin/src/pages/Providers/index.jsx index 6a6a6c0ff0..8860aedb0d 100644 --- a/packages/plugins/users-permissions/admin/src/pages/Providers/index.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/Providers/index.jsx @@ -63,7 +63,7 @@ export const ProvidersPage = () => { const submitMutation = useMutation((body) => put('/users-permissions/providers', body), { async onSuccess() { - await queryClient.invalidateQueries(['users-permissions', 'providers']); + await queryClient.invalidateQueries(['users-permissions', 'get-providers']); toggleNotification({ type: 'success', diff --git a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/ListPage/components/TableBody.jsx b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/ListPage/components/TableBody.jsx index ccb176f3f6..ca714f96fc 100644 --- a/packages/plugins/users-permissions/admin/src/pages/Roles/pages/ListPage/components/TableBody.jsx +++ b/packages/plugins/users-permissions/admin/src/pages/Roles/pages/ListPage/components/TableBody.jsx @@ -13,7 +13,7 @@ const EditLink = styled(Link)` width: 3.2rem; display: flex; justify-content: center; - padding: ${({ theme }) => `${theme.spaces[2]}}`}; + padding: ${({ theme }) => `${theme.spaces[2]}`}; svg { height: 1.6rem; From 068381c9c93c6a13ceef2072bf175979b97270c9 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 24 Jun 2024 22:30:43 +0200 Subject: [PATCH 2/3] fix: content manager error management --- .../core/admin/admin/src/components/Form.tsx | 45 ++++--------------- .../FormInputs/Component/Initializer.tsx | 5 --- .../components/CheckboxWithNumberField.tsx | 10 ++--- .../FormModal/attributes/validation/common.ts | 8 +++- .../admin/src/components/TabForm.tsx | 1 + .../admin/src/translations/en.json | 2 +- 6 files changed, 19 insertions(+), 52 deletions(-) diff --git a/packages/core/admin/admin/src/components/Form.tsx b/packages/core/admin/admin/src/components/Form.tsx index 0a3696e81d..57df4679e2 100644 --- a/packages/core/admin/admin/src/components/Form.tsx +++ b/packages/core/admin/admin/src/components/Form.tsx @@ -670,45 +670,16 @@ const useField = (path: string): FieldValue = const handleChange = useForm('useField', (state) => state.onChange); - const formatNestedErrorMessages = (stateErrors: FormErrors) => { - const nestedErrors: Record = {}; + const error = useForm('useField', (state) => { + const error = getIn(state.errors, path); - Object.entries(stateErrors).forEach(([key, value]) => { - let current = nestedErrors; + if (isErrorMessageDescriptor(error)) { + const { values, ...message } = error; + return formatMessage(message, values); + } - const pathParts = key.split('.'); - pathParts.forEach((part, index) => { - const isLastPart = index === pathParts.length - 1; - - if (isLastPart) { - if (typeof value === 'string') { - // If the value is a translation message object or a string, it should be nested as is - current[part] = value; - } else if (isErrorMessageDescriptor(value)) { - // If the value is a plain object, it should be converted to a string message - current[part] = formatMessage(value); - } else { - // If the value is not an object, it may be an array or a message - setIn(current, part, value); - } - } else { - // Ensure nested structure exists - if (!current[part]) { - const isArray = !isNaN(Number(pathParts[index + 1])); - current[part] = isArray ? [] : {}; - } - - current = current[part]; - } - }); - }); - - return nestedErrors; - }; - - const error = useForm('useField', (state) => - getIn(formatNestedErrorMessages(state.errors), path) - ); + return error; + }); return { initialValue, diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/Component/Initializer.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/Component/Initializer.tsx index 398da61933..007ec7068e 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/Component/Initializer.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/Component/Initializer.tsx @@ -46,11 +46,6 @@ const Initializer = ({ disabled, name, onClick }: InitializerProps) => { - {field.error && ( - - {field.error} - - )} ); }; diff --git a/packages/core/content-type-builder/admin/src/components/CheckboxWithNumberField.tsx b/packages/core/content-type-builder/admin/src/components/CheckboxWithNumberField.tsx index 428a89373e..e2615171b9 100644 --- a/packages/core/content-type-builder/admin/src/components/CheckboxWithNumberField.tsx +++ b/packages/core/content-type-builder/admin/src/components/CheckboxWithNumberField.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import { Box, Checkbox, Field, Flex, NumberInput, TextInput } from '@strapi/design-system'; import { useIntl } from 'react-intl'; @@ -23,7 +21,6 @@ export const CheckboxWithNumberField = ({ value = null, }: CheckboxWithNumberFieldProps) => { const { formatMessage } = useIntl(); - const [showInput, setShowInput] = useState(!!value || value === 0); const label = intlLabel.id ? formatMessage( { id: intlLabel.id, defaultMessage: intlLabel.defaultMessage }, @@ -46,13 +43,12 @@ export const CheckboxWithNumberField = ({ const nextValue = value ? initValue : null; onChange({ target: { name, value: nextValue } }); - setShowInput((prev) => !prev); }} - checked={showInput} + checked={value !== null} > {label} - {showInput && ( + {value !== null && ( {type === 'text' ? ( @@ -70,7 +66,7 @@ export const CheckboxWithNumberField = ({ aria-label={label} disabled={disabled} onValueChange={(value: any) => { - onChange({ target: { name, value, type } }); + onChange({ target: { name, value: value ?? 0, type } }); }} value={value || 0} /> diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/attributes/validation/common.ts b/packages/core/content-type-builder/admin/src/components/FormModal/attributes/validation/common.ts index 4742ad80f0..881af70063 100644 --- a/packages/core/content-type-builder/admin/src/components/FormModal/attributes/validation/common.ts +++ b/packages/core/content-type-builder/admin/src/components/FormModal/attributes/validation/common.ts @@ -81,7 +81,7 @@ const validators = { yup .number() .integer() - .min(0) + .min(1) .when('maxLength', (maxLength, schema) => { if (maxLength) { return schema.max(maxLength, getTrad('error.validation.minSupMax')); @@ -118,7 +118,11 @@ const createTextShape = (usedAttributeNames: Array, reservedNames: Array name: 'isValidRegExpPattern', message: getTrad('error.validation.regex'), test(value) { - return new RegExp(value || '') !== null; + try { + return new RegExp(value || '') !== null; + } catch (e) { + return false; + } }, }) .nullable(), diff --git a/packages/core/content-type-builder/admin/src/components/TabForm.tsx b/packages/core/content-type-builder/admin/src/components/TabForm.tsx index a9337ea78b..1ed815193e 100644 --- a/packages/core/content-type-builder/admin/src/components/TabForm.tsx +++ b/packages/core/content-type-builder/admin/src/components/TabForm.tsx @@ -42,6 +42,7 @@ export const TabForm = ({ {section.items.map((input: any, i: number) => { const key = `${sectionIndex}.${i}`; + /** * Use undefined as the default value because not every input wants a string e.g. Date pickers */ diff --git a/packages/core/content-type-builder/admin/src/translations/en.json b/packages/core/content-type-builder/admin/src/translations/en.json index 06f376df65..d6bf5bb871 100644 --- a/packages/core/content-type-builder/admin/src/translations/en.json +++ b/packages/core/content-type-builder/admin/src/translations/en.json @@ -64,7 +64,7 @@ "error.validation.enum-duplicate": "Duplicate values are not allowed (only alphanumeric characters are taken into account).", "error.validation.enum-empty-string": "Empty strings are not allowed", "error.validation.enum-regex": "At least one value is invalid. Values should have at least one alphabetical character preceding the first occurence of a number.", - "error.validation.minSupMax": "Can't be superior", + "error.validation.minSupMax": "min can't be superior to max", "error.validation.positive": "Must be a positive number", "error.validation.regex": "Regex pattern is invalid", "error.validation.relation.targetAttribute-taken": "This name exists in the target", From 49d033e747e4ba9478e971c081231fb3f9e743ee Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 25 Jun 2024 12:01:15 +0200 Subject: [PATCH 3/3] fix: cm component validation --- .../core/admin/admin/src/components/Form.tsx | 2 +- .../admin/src/components/tests/Form.test.tsx | 44 +++++++++++++++---- .../admin/src/hooks/useAPIErrorHandler.ts | 8 ++-- .../pages/AuditLogs/tests/ListPage.test.tsx | 1 - .../src/hooks/tests/useDocument.test.tsx | 12 ++++- .../FormInputs/DynamicZone/Field.tsx | 5 +-- .../EditView/components/InputRenderer.tsx | 4 +- .../admin/src/utils/validation.ts | 43 ++++++++++++++++-- .../admin/src/translations/en.json | 2 +- 9 files changed, 93 insertions(+), 28 deletions(-) diff --git a/packages/core/admin/admin/src/components/Form.tsx b/packages/core/admin/admin/src/components/Form.tsx index 57df4679e2..146a70f7e9 100644 --- a/packages/core/admin/admin/src/components/Form.tsx +++ b/packages/core/admin/admin/src/components/Form.tsx @@ -619,7 +619,7 @@ const reducer = ( draft.values = setIn( state.values, action.payload.field, - newValue.length > 0 ? newValue : undefined + newValue.length > 0 ? newValue : [] ); break; diff --git a/packages/core/admin/admin/src/components/tests/Form.test.tsx b/packages/core/admin/admin/src/components/tests/Form.test.tsx index 93e3e21206..b21d734170 100644 --- a/packages/core/admin/admin/src/components/tests/Form.test.tsx +++ b/packages/core/admin/admin/src/components/tests/Form.test.tsx @@ -15,8 +15,22 @@ describe('useField hook', () => { it('formats and returns nested error messages correctly for field constraints', () => { const expectedError = 'This attribute must be unique'; const initialErrors = { - 'repeatable.0.nestedUnique.TextShort': 'Another error message', - 'repeatable.1.nestedUnique.nestedLevelOne.nestedLevelTwo.Unique': expectedError, + repeatable: [ + { + nestedUnique: { + TextShort: 'Another error message', + }, + }, + { + nestedUnique: { + nestedLevelOne: { + nestedLevelTwo: { + Unique: expectedError, + }, + }, + }, + }, + ], }; const { result } = renderHook( @@ -35,7 +49,9 @@ describe('useField hook', () => { defaultMessage: 'This attribute must be unique', }; const initialErrors = { - 'nested.uniqueAttribute': messageDescriptor, + nested: { + uniqueAttribute: messageDescriptor, + }, }; const { result } = renderHook(() => useField('nested.uniqueAttribute'), { @@ -51,9 +67,11 @@ describe('useField hook', () => { defaultMessage: 'Mixed error message', }; const initialErrors = { - 'mixed.errorField': messageDescriptor, - 'mixed.stringError': 'String error message', - 'mixed.otherError': 123, // Non-string, non-descriptor error + mixed: { + errorField: messageDescriptor, + stringError: 'String error message', + otherError: 123, // Non-string, non-descriptor error + }, }; const { result } = renderHook(() => useField('mixed.otherError'), { @@ -65,8 +83,14 @@ describe('useField hook', () => { it('handles errors associated with array indices', () => { const initialErrors = { - 'array.0.field': 'Error on first array item', - 'array.1.field': 'Error on second array item', + array: [ + { + field: 'Error on first array item', + }, + { + field: 'Error on second array item', + }, + ], }; const { result } = renderHook(() => useField('array.0.field'), { @@ -88,7 +112,9 @@ describe('useField hook', () => { it('returns undefined for non-existent error paths', () => { const initialErrors = { - 'valid.path': 'Error message', + valid: { + path: 'Error message', + }, }; const { result } = renderHook(() => useField('invalid.path'), { diff --git a/packages/core/admin/admin/src/hooks/useAPIErrorHandler.ts b/packages/core/admin/admin/src/hooks/useAPIErrorHandler.ts index 78d531d741..76823af38e 100644 --- a/packages/core/admin/admin/src/hooks/useAPIErrorHandler.ts +++ b/packages/core/admin/admin/src/hooks/useAPIErrorHandler.ts @@ -5,6 +5,7 @@ import { IntlFormatters, useIntl } from 'react-intl'; import { FetchError } from '../utils/getFetchClient'; import { getPrefixedId } from '../utils/getPrefixedId'; import { NormalizeErrorOptions, normalizeAPIError } from '../utils/normalizeAPIError'; +import { setIn } from '../utils/objects'; import type { errors } from '@strapi/utils'; @@ -97,7 +98,7 @@ that has been thrown. * const { get } = useFetchClient(); * const { formatAPIError } = useAPIErrorHandler(getTrad); * const { toggleNotification } = useNotification(); - * + * * const handleDeleteItem = async () => { * try { * return await get('/admin'); @@ -156,10 +157,7 @@ export function useAPIErrorHandler( return validationErrors.reduce((acc, err) => { const { path, message } = err; - return { - ...acc, - [path.join('.')]: message, - }; + return setIn(acc, path.join('.'), message); }, {}); } else { const details = error.details as Record; diff --git a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/tests/ListPage.test.tsx b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/tests/ListPage.test.tsx index 8d68d111e6..eca9324067 100644 --- a/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/tests/ListPage.test.tsx +++ b/packages/core/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/tests/ListPage.test.tsx @@ -28,7 +28,6 @@ describe('ADMIN | Pages | AUDIT LOGS | ListPage', () => { 'Action', 'Date', 'User', - 'Actions', 'Admin logout', 'October 31, 2023, 15:56:54', 'Create user', diff --git a/packages/core/content-manager/admin/src/hooks/tests/useDocument.test.tsx b/packages/core/content-manager/admin/src/hooks/tests/useDocument.test.tsx index 39da3aa84b..ec5b4606b2 100644 --- a/packages/core/content-manager/admin/src/hooks/tests/useDocument.test.tsx +++ b/packages/core/content-manager/admin/src/hooks/tests/useDocument.test.tsx @@ -78,7 +78,11 @@ describe('useDocument', () => { postal_code: 'N2', notrepeat_req: {}, city: 'London', - repeat_req: [], + repeat_req: [ + { + name: 'toto', + }, + ], }) ).toBeNull(); @@ -89,7 +93,11 @@ describe('useDocument', () => { notrepeat_req: {}, postal_code: 12, city: 'London', - repeat_req: [], + repeat_req: [ + { + name: 'toto', + }, + ], }) ).toMatchInlineSnapshot(` { diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/DynamicZone/Field.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/DynamicZone/Field.tsx index c7d27e69dd..1488dcd476 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/DynamicZone/Field.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/DynamicZone/Field.tsx @@ -192,10 +192,7 @@ const DynamicZone = ({ removeFieldRow(name, currentIndex); }; - const hasError = - error !== undefined || - dynamicDisplayedComponentsLength < min || - dynamicDisplayedComponentsLength > max; + const hasError = error !== undefined; const renderButtonLabel = () => { if (addComponentIsOpen) { diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx index 11aaa5d3e4..a41054bfd7 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx @@ -179,7 +179,9 @@ const useFieldHint = (hint: ReactNode = undefined, attribute: Schema.Attribute.A return hint; } - const units = !['biginteger', 'integer', 'number'].includes(attribute.type) + const units = !['biginteger', 'integer', 'number', 'dynamiczone', 'component'].includes( + attribute.type + ) ? formatMessage( { id: 'content-manager.form.Input.hint.character.unit', diff --git a/packages/core/content-manager/admin/src/utils/validation.ts b/packages/core/content-manager/admin/src/utils/validation.ts index dd7c2254de..c4aa4b16c7 100644 --- a/packages/core/content-manager/admin/src/utils/validation.ts +++ b/packages/core/content-manager/admin/src/utils/validation.ts @@ -216,11 +216,17 @@ type ValidationFn = ( ) => (schema: TSchema) => TSchema; const addRequiredValidation: ValidationFn = (attribute) => (schema) => { + if ( + ((attribute.type === 'component' && attribute.repeatable) || + attribute.type === 'dynamiczone') && + attribute.required && + 'min' in schema + ) { + return schema.min(1, translatedErrors.required); + } + if (attribute.required && attribute.type !== 'relation') { - return schema.required({ - id: translatedErrors.required.id, - defaultMessage: 'This field is required.', - }); + return schema.required(translatedErrors.required); } return schema?.nullable @@ -277,6 +283,35 @@ const addMinValidation: ValidationFn = if ('min' in attribute) { const min = toInteger(attribute.min); + if ( + (attribute.type === 'component' && attribute.repeatable) || + attribute.type === 'dynamiczone' + ) { + if (!attribute.required && 'test' in schema && min) { + // @ts-expect-error - We know the schema is an array here but ts doesn't know. + return schema.test( + 'custom-min', + { + ...translatedErrors.min, + values: { + min: attribute.min, + }, + }, + (value: Array) => { + if (!value) { + return true; + } + + if (Array.isArray(value) && value.length === 0) { + return true; + } + + return value.length >= min; + } + ); + } + } + if ('min' in schema && min) { return schema.min(min, { ...translatedErrors.min, diff --git a/packages/core/content-type-builder/admin/src/translations/en.json b/packages/core/content-type-builder/admin/src/translations/en.json index d6bf5bb871..5a31d434b0 100644 --- a/packages/core/content-type-builder/admin/src/translations/en.json +++ b/packages/core/content-type-builder/admin/src/translations/en.json @@ -64,7 +64,7 @@ "error.validation.enum-duplicate": "Duplicate values are not allowed (only alphanumeric characters are taken into account).", "error.validation.enum-empty-string": "Empty strings are not allowed", "error.validation.enum-regex": "At least one value is invalid. Values should have at least one alphabetical character preceding the first occurence of a number.", - "error.validation.minSupMax": "min can't be superior to max", + "error.validation.minSupMax": "Min can't be superior to max", "error.validation.positive": "Must be a positive number", "error.validation.regex": "Regex pattern is invalid", "error.validation.relation.targetAttribute-taken": "This name exists in the target",