Merge pull request #20615 from strapi/v5/cm-fixes

fix: several UI issues a typos
This commit is contained in:
Alexandre BODIN 2024-06-25 16:58:11 +02:00 committed by GitHub
commit d0b51fbf84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 147 additions and 117 deletions

View File

@ -619,7 +619,7 @@ const reducer = <TFormValues extends FormValues = FormValues>(
draft.values = setIn( draft.values = setIn(
state.values, state.values,
action.payload.field, action.payload.field,
newValue.length > 0 ? newValue : undefined newValue.length > 0 ? newValue : []
); );
break; break;
@ -670,45 +670,16 @@ const useField = <TValue = any,>(path: string): FieldValue<TValue | undefined> =
const handleChange = useForm('useField', (state) => state.onChange); const handleChange = useForm('useField', (state) => state.onChange);
const formatNestedErrorMessages = (stateErrors: FormErrors<FormValues>) => { const error = useForm('useField', (state) => {
const nestedErrors: Record<string, any> = {}; const error = getIn(state.errors, path);
Object.entries(stateErrors).forEach(([key, value]) => { if (isErrorMessageDescriptor(error)) {
let current = nestedErrors; 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 error;
}
}); });
});
return nestedErrors;
};
const error = useForm('useField', (state) =>
getIn(formatNestedErrorMessages(state.errors), path)
);
return { return {
initialValue, initialValue,

View File

@ -15,8 +15,22 @@ describe('useField hook', () => {
it('formats and returns nested error messages correctly for field constraints', () => { it('formats and returns nested error messages correctly for field constraints', () => {
const expectedError = 'This attribute must be unique'; const expectedError = 'This attribute must be unique';
const initialErrors = { const initialErrors = {
'repeatable.0.nestedUnique.TextShort': 'Another error message', repeatable: [
'repeatable.1.nestedUnique.nestedLevelOne.nestedLevelTwo.Unique': expectedError, {
nestedUnique: {
TextShort: 'Another error message',
},
},
{
nestedUnique: {
nestedLevelOne: {
nestedLevelTwo: {
Unique: expectedError,
},
},
},
},
],
}; };
const { result } = renderHook( const { result } = renderHook(
@ -35,7 +49,9 @@ describe('useField hook', () => {
defaultMessage: 'This attribute must be unique', defaultMessage: 'This attribute must be unique',
}; };
const initialErrors = { const initialErrors = {
'nested.uniqueAttribute': messageDescriptor, nested: {
uniqueAttribute: messageDescriptor,
},
}; };
const { result } = renderHook(() => useField('nested.uniqueAttribute'), { const { result } = renderHook(() => useField('nested.uniqueAttribute'), {
@ -51,9 +67,11 @@ describe('useField hook', () => {
defaultMessage: 'Mixed error message', defaultMessage: 'Mixed error message',
}; };
const initialErrors = { const initialErrors = {
'mixed.errorField': messageDescriptor, mixed: {
'mixed.stringError': 'String error message', errorField: messageDescriptor,
'mixed.otherError': 123, // Non-string, non-descriptor error stringError: 'String error message',
otherError: 123, // Non-string, non-descriptor error
},
}; };
const { result } = renderHook(() => useField('mixed.otherError'), { const { result } = renderHook(() => useField('mixed.otherError'), {
@ -65,8 +83,14 @@ describe('useField hook', () => {
it('handles errors associated with array indices', () => { it('handles errors associated with array indices', () => {
const initialErrors = { const initialErrors = {
'array.0.field': 'Error on first array item', array: [
'array.1.field': 'Error on second array item', {
field: 'Error on first array item',
},
{
field: 'Error on second array item',
},
],
}; };
const { result } = renderHook(() => useField('array.0.field'), { const { result } = renderHook(() => useField('array.0.field'), {
@ -88,7 +112,9 @@ describe('useField hook', () => {
it('returns undefined for non-existent error paths', () => { it('returns undefined for non-existent error paths', () => {
const initialErrors = { const initialErrors = {
'valid.path': 'Error message', valid: {
path: 'Error message',
},
}; };
const { result } = renderHook(() => useField('invalid.path'), { const { result } = renderHook(() => useField('invalid.path'), {

View File

@ -5,6 +5,7 @@ import { IntlFormatters, useIntl } from 'react-intl';
import { FetchError } from '../utils/getFetchClient'; import { FetchError } from '../utils/getFetchClient';
import { getPrefixedId } from '../utils/getPrefixedId'; import { getPrefixedId } from '../utils/getPrefixedId';
import { NormalizeErrorOptions, normalizeAPIError } from '../utils/normalizeAPIError'; import { NormalizeErrorOptions, normalizeAPIError } from '../utils/normalizeAPIError';
import { setIn } from '../utils/objects';
import type { errors } from '@strapi/utils'; import type { errors } from '@strapi/utils';
@ -156,10 +157,7 @@ export function useAPIErrorHandler(
return validationErrors.reduce((acc, err) => { return validationErrors.reduce((acc, err) => {
const { path, message } = err; const { path, message } = err;
return { return setIn(acc, path.join('.'), message);
...acc,
[path.join('.')]: message,
};
}, {}); }, {});
} else { } else {
const details = error.details as Record<string, string[]>; const details = error.details as Record<string, string[]>;

View File

@ -286,6 +286,7 @@ const EventsRow = ({
{events.map((event) => { {events.map((event) => {
return ( return (
<Td key={event} textAlign="center"> <Td key={event} textAlign="center">
<Flex width="100%" justifyContent="center">
<Checkbox <Checkbox
disabled={disabledEvents.includes(event)} disabled={disabledEvents.includes(event)}
aria-label={event} aria-label={event}
@ -293,6 +294,7 @@ const EventsRow = ({
checked={inputValue.includes(event)} checked={inputValue.includes(event)}
onCheckedChange={(value) => handleSelect(event, !!value)} onCheckedChange={(value) => handleSelect(event, !!value)}
/> />
</Flex>
</Td> </Td>
); );
})} })}

View File

@ -13,10 +13,15 @@ import {
} from '@strapi/design-system'; } from '@strapi/design-system';
import { Minus, Plus } from '@strapi/icons'; import { Minus, Plus } from '@strapi/icons';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { styled } from 'styled-components';
import { useField, useForm } from '../../../../../components/Form'; import { useField, useForm } from '../../../../../components/Form';
import { StringInput } from '../../../../../components/FormInputs/String'; import { StringInput } from '../../../../../components/FormInputs/String';
const AddHeaderButton = styled(TextButton)`
cursor: pointer;
`;
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
* HeadersInput * HeadersInput
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
@ -44,7 +49,7 @@ const HeadersInput = () => {
<Box padding={8} background="neutral100" hasRadius> <Box padding={8} background="neutral100" hasRadius>
{value.map((_, index) => { {value.map((_, index) => {
return ( return (
<Grid.Root key={index} gap={4}> <Grid.Root key={index} gap={4} padding={2}>
<Grid.Item col={6}> <Grid.Item col={6}>
<HeaderCombobox <HeaderCombobox
name={`headers.${index}.key`} name={`headers.${index}.key`}
@ -92,8 +97,11 @@ const HeadersInput = () => {
</Flex> </Flex>
</Flex> </Flex>
</Grid.Item> </Grid.Item>
<Grid.Item col={12}> </Grid.Root>
<TextButton );
})}
<Box paddingTop={4}>
<AddHeaderButton
type="button" type="button"
onClick={() => { onClick={() => {
addFieldRow('headers', { key: '', value: '' }); addFieldRow('headers', { key: '', value: '' });
@ -104,11 +112,8 @@ const HeadersInput = () => {
id: 'Settings.webhooks.create.header', id: 'Settings.webhooks.create.header',
defaultMessage: 'Create new header', defaultMessage: 'Create new header',
})} })}
</TextButton> </AddHeaderButton>
</Grid.Item> </Box>
</Grid.Root>
);
})}
</Box> </Box>
</Flex> </Flex>
); );

View File

@ -72,14 +72,6 @@ const ListPage = () => {
// In this case, the passed parameter cannot and shouldn't be something else than User // In this case, the passed parameter cannot and shouldn't be something else than User
cellFormatter: ({ user }) => (user ? user.displayName : ''), cellFormatter: ({ user }) => (user ? user.displayName : ''),
}, },
{
name: 'actions',
label: formatMessage({
id: 'Settings.permissions.auditLogs.actions',
defaultMessage: 'Actions',
}),
sortable: false,
},
]; ];
if (hasError) { if (hasError) {
@ -170,8 +162,6 @@ const ListPage = () => {
</Typography> </Typography>
</Table.Cell> </Table.Cell>
); );
case 'actions':
return null;
default: default:
return ( return (
<Table.Cell key={name}> <Table.Cell key={name}>
@ -202,8 +192,9 @@ const ListPage = () => {
</Table.Body> </Table.Body>
</Table.Content> </Table.Content>
</Table.Root> </Table.Root>
<Pagination.Root {...auditLogs?.pagination} defaultPageSize={24}>
<Pagination.PageSize options={['12', '24', '50', '100']} /> <Pagination.Root {...auditLogs?.pagination}>
<Pagination.PageSize />
<Pagination.Links /> <Pagination.Links />
</Pagination.Root> </Pagination.Root>
</Layouts.Content> </Layouts.Content>

View File

@ -28,7 +28,6 @@ describe('ADMIN | Pages | AUDIT LOGS | ListPage', () => {
'Action', 'Action',
'Date', 'Date',
'User', 'User',
'Actions',
'Admin logout', 'Admin logout',
'October 31, 2023, 15:56:54', 'October 31, 2023, 15:56:54',
'Create user', 'Create user',

View File

@ -78,7 +78,11 @@ describe('useDocument', () => {
postal_code: 'N2', postal_code: 'N2',
notrepeat_req: {}, notrepeat_req: {},
city: 'London', city: 'London',
repeat_req: [], repeat_req: [
{
name: 'toto',
},
],
}) })
).toBeNull(); ).toBeNull();
@ -89,7 +93,11 @@ describe('useDocument', () => {
notrepeat_req: {}, notrepeat_req: {},
postal_code: 12, postal_code: 12,
city: 'London', city: 'London',
repeat_req: [], repeat_req: [
{
name: 'toto',
},
],
}) })
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
{ {

View File

@ -46,11 +46,6 @@ const Initializer = ({ disabled, name, onClick }: InitializerProps) => {
</Flex> </Flex>
</Flex> </Flex>
</Box> </Box>
{field.error && (
<Typography textColor="danger600" variant="pi">
{field.error}
</Typography>
)}
</> </>
); );
}; };

View File

@ -192,10 +192,7 @@ const DynamicZone = ({
removeFieldRow(name, currentIndex); removeFieldRow(name, currentIndex);
}; };
const hasError = const hasError = error !== undefined;
error !== undefined ||
dynamicDisplayedComponentsLength < min ||
dynamicDisplayedComponentsLength > max;
const renderButtonLabel = () => { const renderButtonLabel = () => {
if (addComponentIsOpen) { if (addComponentIsOpen) {

View File

@ -179,7 +179,9 @@ const useFieldHint = (hint: ReactNode = undefined, attribute: Schema.Attribute.A
return hint; return hint;
} }
const units = !['biginteger', 'integer', 'number'].includes(attribute.type) const units = !['biginteger', 'integer', 'number', 'dynamiczone', 'component'].includes(
attribute.type
)
? formatMessage( ? formatMessage(
{ {
id: 'content-manager.form.Input.hint.character.unit', id: 'content-manager.form.Input.hint.character.unit',

View File

@ -216,11 +216,17 @@ type ValidationFn = (
) => <TSchema extends AnySchema>(schema: TSchema) => TSchema; ) => <TSchema extends AnySchema>(schema: TSchema) => TSchema;
const addRequiredValidation: ValidationFn = (attribute) => (schema) => { 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') { if (attribute.required && attribute.type !== 'relation') {
return schema.required({ return schema.required(translatedErrors.required);
id: translatedErrors.required.id,
defaultMessage: 'This field is required.',
});
} }
return schema?.nullable return schema?.nullable
@ -277,6 +283,35 @@ const addMinValidation: ValidationFn =
if ('min' in attribute) { if ('min' in attribute) {
const min = toInteger(attribute.min); 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<unknown>) => {
if (!value) {
return true;
}
if (Array.isArray(value) && value.length === 0) {
return true;
}
return value.length >= min;
}
);
}
}
if ('min' in schema && min) { if ('min' in schema && min) {
return schema.min(min, { return schema.min(min, {
...translatedErrors.min, ...translatedErrors.min,

View File

@ -1,5 +1,3 @@
import { useState } from 'react';
import { Box, Checkbox, Field, Flex, NumberInput, TextInput } from '@strapi/design-system'; import { Box, Checkbox, Field, Flex, NumberInput, TextInput } from '@strapi/design-system';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -23,7 +21,6 @@ export const CheckboxWithNumberField = ({
value = null, value = null,
}: CheckboxWithNumberFieldProps) => { }: CheckboxWithNumberFieldProps) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [showInput, setShowInput] = useState(!!value || value === 0);
const label = intlLabel.id const label = intlLabel.id
? formatMessage( ? formatMessage(
{ id: intlLabel.id, defaultMessage: intlLabel.defaultMessage }, { id: intlLabel.id, defaultMessage: intlLabel.defaultMessage },
@ -46,13 +43,12 @@ export const CheckboxWithNumberField = ({
const nextValue = value ? initValue : null; const nextValue = value ? initValue : null;
onChange({ target: { name, value: nextValue } }); onChange({ target: { name, value: nextValue } });
setShowInput((prev) => !prev);
}} }}
checked={showInput} checked={value !== null}
> >
{label} {label}
</Checkbox> </Checkbox>
{showInput && ( {value !== null && (
<Box paddingLeft={6} style={{ maxWidth: '200px' }}> <Box paddingLeft={6} style={{ maxWidth: '200px' }}>
{type === 'text' ? ( {type === 'text' ? (
<Field.Root error={errorMessage} name={name}> <Field.Root error={errorMessage} name={name}>
@ -70,7 +66,7 @@ export const CheckboxWithNumberField = ({
aria-label={label} aria-label={label}
disabled={disabled} disabled={disabled}
onValueChange={(value: any) => { onValueChange={(value: any) => {
onChange({ target: { name, value, type } }); onChange({ target: { name, value: value ?? 0, type } });
}} }}
value={value || 0} value={value || 0}
/> />

View File

@ -81,7 +81,7 @@ const validators = {
yup yup
.number() .number()
.integer() .integer()
.min(0) .min(1)
.when('maxLength', (maxLength, schema) => { .when('maxLength', (maxLength, schema) => {
if (maxLength) { if (maxLength) {
return schema.max(maxLength, getTrad('error.validation.minSupMax')); return schema.max(maxLength, getTrad('error.validation.minSupMax'));
@ -118,7 +118,11 @@ const createTextShape = (usedAttributeNames: Array<string>, reservedNames: Array
name: 'isValidRegExpPattern', name: 'isValidRegExpPattern',
message: getTrad('error.validation.regex'), message: getTrad('error.validation.regex'),
test(value) { test(value) {
try {
return new RegExp(value || '') !== null; return new RegExp(value || '') !== null;
} catch (e) {
return false;
}
}, },
}) })
.nullable(), .nullable(),

View File

@ -42,6 +42,7 @@ export const TabForm = ({
<Grid.Root gap={4}> <Grid.Root gap={4}>
{section.items.map((input: any, i: number) => { {section.items.map((input: any, i: number) => {
const key = `${sectionIndex}.${i}`; const key = `${sectionIndex}.${i}`;
/** /**
* Use undefined as the default value because not every input wants a string e.g. Date pickers * Use undefined as the default value because not every input wants a string e.g. Date pickers
*/ */

View File

@ -64,7 +64,7 @@
"error.validation.enum-duplicate": "Duplicate values are not allowed (only alphanumeric characters are taken into account).", "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-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.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.positive": "Must be a positive number",
"error.validation.regex": "Regex pattern is invalid", "error.validation.regex": "Regex pattern is invalid",
"error.validation.relation.targetAttribute-taken": "This name exists in the target", "error.validation.relation.targetAttribute-taken": "This name exists in the target",

View File

@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Box, Checkbox, Flex, Typography, Grid, VisuallyHidden } from '@strapi/design-system'; 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 get from 'lodash/get';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -103,7 +103,7 @@ const SubCategory = ({ subCategory }) => {
} }
)} )}
</VisuallyHidden> </VisuallyHidden>
<CogIcon id="cog" /> <Cog id="cog" />
</button> </button>
</CheckboxWrapper> </CheckboxWrapper>
</Grid.Item> </Grid.Item>

View File

@ -63,7 +63,7 @@ export const ProvidersPage = () => {
const submitMutation = useMutation((body) => put('/users-permissions/providers', body), { const submitMutation = useMutation((body) => put('/users-permissions/providers', body), {
async onSuccess() { async onSuccess() {
await queryClient.invalidateQueries(['users-permissions', 'providers']); await queryClient.invalidateQueries(['users-permissions', 'get-providers']);
toggleNotification({ toggleNotification({
type: 'success', type: 'success',

View File

@ -13,7 +13,7 @@ const EditLink = styled(Link)`
width: 3.2rem; width: 3.2rem;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: ${({ theme }) => `${theme.spaces[2]}}`}; padding: ${({ theme }) => `${theme.spaces[2]}`};
svg { svg {
height: 1.6rem; height: 1.6rem;