mirror of
https://github.com/strapi/strapi.git
synced 2025-07-13 12:02:10 +00:00
Merge pull request #20615 from strapi/v5/cm-fixes
fix: several UI issues a typos
This commit is contained in:
commit
d0b51fbf84
@ -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,
|
||||
|
@ -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'), {
|
||||
|
@ -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[]>;
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -28,7 +28,6 @@ describe('ADMIN | Pages | AUDIT LOGS | ListPage', () => {
|
||||
'Action',
|
||||
'Date',
|
||||
'User',
|
||||
'Actions',
|
||||
'Admin logout',
|
||||
'October 31, 2023, 15:56:54',
|
||||
'Create user',
|
||||
|
@ -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(`
|
||||
{
|
||||
|
@ -46,11 +46,6 @@ const Initializer = ({ disabled, name, onClick }: InitializerProps) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
{field.error && (
|
||||
<Typography textColor="danger600" variant="pi">
|
||||
{field.error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user