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(
state.values,
action.payload.field,
newValue.length > 0 ? newValue : undefined
newValue.length > 0 ? newValue : []
);
break;
@ -670,45 +670,16 @@ const useField = <TValue = any,>(path: string): FieldValue<TValue | undefined> =
const handleChange = useForm('useField', (state) => state.onChange);
const formatNestedErrorMessages = (stateErrors: FormErrors<FormValues>) => {
const nestedErrors: Record<string, any> = {};
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,

View File

@ -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'), {

View File

@ -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<string, string[]>;

View File

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

View File

@ -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 = () => {
<Box padding={8} background="neutral100" hasRadius>
{value.map((_, index) => {
return (
<Grid.Root key={index} gap={4}>
<Grid.Root key={index} gap={4} padding={2}>
<Grid.Item col={6}>
<HeaderCombobox
name={`headers.${index}.key`}
@ -92,23 +97,23 @@ const HeadersInput = () => {
</Flex>
</Flex>
</Grid.Item>
<Grid.Item col={12}>
<TextButton
type="button"
onClick={() => {
addFieldRow('headers', { key: '', value: '' });
}}
startIcon={<Plus />}
>
{formatMessage({
id: 'Settings.webhooks.create.header',
defaultMessage: 'Create new header',
})}
</TextButton>
</Grid.Item>
</Grid.Root>
);
})}
<Box paddingTop={4}>
<AddHeaderButton
type="button"
onClick={() => {
addFieldRow('headers', { key: '', value: '' });
}}
startIcon={<Plus />}
>
{formatMessage({
id: 'Settings.webhooks.create.header',
defaultMessage: 'Create new header',
})}
</AddHeaderButton>
</Box>
</Box>
</Flex>
);

View File

@ -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 = () => {
</Typography>
</Table.Cell>
);
case 'actions':
return null;
default:
return (
<Table.Cell key={name}>
@ -202,8 +192,9 @@ const ListPage = () => {
</Table.Body>
</Table.Content>
</Table.Root>
<Pagination.Root {...auditLogs?.pagination} defaultPageSize={24}>
<Pagination.PageSize options={['12', '24', '50', '100']} />
<Pagination.Root {...auditLogs?.pagination}>
<Pagination.PageSize />
<Pagination.Links />
</Pagination.Root>
</Layouts.Content>

View File

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

View File

@ -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(`
{

View File

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

View File

@ -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) {

View File

@ -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',

View File

@ -216,11 +216,17 @@ type ValidationFn = (
) => <TSchema extends AnySchema>(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<unknown>) => {
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,

View File

@ -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}
</Checkbox>
{showInput && (
{value !== null && (
<Box paddingLeft={6} style={{ maxWidth: '200px' }}>
{type === 'text' ? (
<Field.Root error={errorMessage} name={name}>
@ -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}
/>

View File

@ -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<string>, 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(),

View File

@ -42,6 +42,7 @@ export const TabForm = ({
<Grid.Root gap={4}>
{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
*/

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-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",

View File

@ -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 }) => {
}
)}
</VisuallyHidden>
<CogIcon id="cog" />
<Cog id="cog" />
</button>
</CheckboxWrapper>
</Grid.Item>

View File

@ -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',

View File

@ -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;