Merge pull request #1719 from payloadcms/feat/1695-nullable-localized-array-and-blocks

Feat: allow null for non-default locales on arrays and blocks
This commit is contained in:
Jarrod Flesch
2023-01-10 11:23:46 -05:00
committed by GitHub
44 changed files with 448 additions and 139 deletions

View File

@@ -6,7 +6,7 @@ import './index.scss';
const baseClass = 'banner';
const Banner: React.FC<Props> = ({
export const Banner: React.FC<Props> = ({
children,
className,
to,

View File

@@ -12,4 +12,4 @@
&::after {
border-top-color: var(--theme-error-500);
}
}
}

View File

@@ -105,11 +105,16 @@ export const addFieldStatePromise = async ({
await Promise.all(promises);
// Add values to field state
fieldState.value = arrayValue.length;
fieldState.initialValue = arrayValue.length;
if (valueWithDefault === null) {
fieldState.value = null;
fieldState.initialValue = null;
} else {
fieldState.value = arrayValue.length;
fieldState.initialValue = arrayValue.length;
if (arrayValue.length > 0) {
fieldState.disableFormData = true;
if (arrayValue.length > 0) {
fieldState.disableFormData = true;
}
}
// Add field to state
@@ -161,13 +166,17 @@ export const addFieldStatePromise = async ({
}
});
await Promise.all(promises);
// Add values to field state
fieldState.value = blocksValue.length;
fieldState.initialValue = blocksValue.length;
if (valueWithDefault === null) {
fieldState.value = null;
fieldState.initialValue = null;
} else {
fieldState.value = blocksValue.length;
fieldState.initialValue = blocksValue.length;
if (blocksValue.length > 0) {
fieldState.disableFormData = true;
if (blocksValue.length > 0) {
fieldState.disableFormData = true;
}
}
// Add field to state

View File

@@ -0,0 +1,59 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { CheckboxInput } from '../field-types/Checkbox/Input';
import { Banner } from '../../elements/Banner';
import { useLocale } from '../../utilities/Locale';
import { useConfig } from '../../utilities/Config';
import { useForm } from '../Form/context';
type NullifyFieldProps = {
path: string
fieldValue?: null | [] | number
}
export const NullifyField: React.FC<NullifyFieldProps> = ({ path, fieldValue }) => {
const { dispatchFields, setModified } = useForm();
const currentLocale = useLocale();
const { localization } = useConfig();
const [checked, setChecked] = React.useState<boolean>(typeof fieldValue !== 'number');
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
const { t } = useTranslation('general');
const onChange = () => {
const useFallback = !checked;
dispatchFields({
type: 'UPDATE',
path,
value: useFallback ? null : (fieldValue || 0),
});
setModified(true);
setChecked(useFallback);
};
if (currentLocale === defaultLocale) {
// hide when editing default locale
return null;
}
if (fieldValue) {
let hideCheckbox = false;
if (typeof fieldValue === 'number' && fieldValue > 0) hideCheckbox = true;
if (Array.isArray(fieldValue) && fieldValue.length > 0) hideCheckbox = true;
if (hideCheckbox) {
if (checked) setChecked(false); // uncheck when field has value
return null;
}
}
return (
<Banner>
<CheckboxInput
id={`field-${path.replace(/\./gi, '__')}`}
onToggle={onChange}
label={t('fallbackToDefaultLocale')}
checked={checked}
/>
</Banner>
);
};

View File

@@ -25,6 +25,8 @@ import HiddenInput from '../HiddenInput';
import { RowLabel } from '../../RowLabel';
import { getTranslation } from '../../../../../utilities/getTranslation';
import { createNestedFieldPath } from '../../Form/createNestedFieldPath';
import { useConfig } from '../../../utilities/Config';
import { NullifyField } from '../../NullifyField';
import './index.scss';
@@ -69,6 +71,15 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const locale = useLocale();
const operation = useOperation();
const { t, i18n } = useTranslation('fields');
const { localization } = useConfig();
const checkSkipValidation = useCallback((value) => {
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
const isEditingDefaultLocale = locale === defaultLocale;
if (value === null && !isEditingDefaultLocale) return true;
return false;
}, [locale, localization]);
// Handle labeling for Arrays, Global Arrays, and Blocks
const getLabels = (p: Props) => {
@@ -82,14 +93,15 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const { dispatchFields, setModified } = formContext;
const memoizedValidate = useCallback((value, options) => {
if (checkSkipValidation(value)) return true;
return validate(value, { ...options, minRows, maxRows, required });
}, [maxRows, minRows, required, validate]);
}, [maxRows, minRows, required, validate, checkSkipValidation]);
const {
showError,
errorMessage,
value,
} = useField({
} = useField<number>({
path,
validate: memoizedValidate,
condition,
@@ -252,6 +264,12 @@ const ArrayFieldType: React.FC<Props> = (props) => {
description={description}
/>
</header>
<NullifyField
path={path}
fieldValue={value}
/>
<Droppable droppableId="array-drop">
{(provided) => (
<div
@@ -322,23 +340,28 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</Draggable>
);
})}
{(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error">
{t('validation:requiresAtLeast', {
count: minRows,
label: getTranslation(minRows ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
})}
</Banner>
)}
{(rows.length === 0 && readOnly) && (
<Banner>
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
</Banner>
{!checkSkipValidation(value) && (
<React.Fragment>
{(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error">
{t('validation:requiresAtLeast', {
count: minRows,
label: getTranslation(minRows ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
})}
</Banner>
)}
{(rows.length === 0 && readOnly) && (
<Banner>
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
</Banner>
)}
</React.Fragment>
)}
{provided.placeholder}
</div>
)}
</Droppable>
{(!readOnly && !hasMaxRows) && (
<div className={`${baseClass}__add-button-wrap`}>
<Button
@@ -352,6 +375,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</Button>
</div>
)}
</div>
</DragDropContext>
);

View File

@@ -28,6 +28,8 @@ import Pill from '../../../elements/Pill';
import { scrollToID } from '../../../../utilities/scrollToID';
import HiddenInput from '../HiddenInput';
import { getTranslation } from '../../../../../utilities/getTranslation';
import { NullifyField } from '../../NullifyField';
import { useConfig } from '../../../utilities/Config';
import { createNestedFieldPath } from '../../Form/createNestedFieldPath';
import './index.scss';
@@ -74,11 +76,20 @@ const BlocksField: React.FC<Props> = (props) => {
const operation = useOperation();
const { dispatchFields, setModified } = formContext;
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
const { localization } = useConfig();
const checkSkipValidation = useCallback((value) => {
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
const isEditingDefaultLocale = locale === defaultLocale;
if (value === null && !isEditingDefaultLocale) return true;
return false;
}, [locale, localization]);
const memoizedValidate = useCallback((value, options) => {
if (checkSkipValidation(value)) return true;
return validate(value, { ...options, minRows, maxRows, required });
}, [maxRows, minRows, required, validate]);
}, [maxRows, minRows, required, validate, checkSkipValidation]);
const {
showError,
@@ -252,6 +263,11 @@ const BlocksField: React.FC<Props> = (props) => {
/>
</header>
<NullifyField
path={path}
fieldValue={value}
/>
<Droppable
droppableId="blocks-drop"
isDropDisabled={readOnly}
@@ -359,18 +375,23 @@ const BlocksField: React.FC<Props> = (props) => {
return null;
})}
{(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error">
{t('validation:requiresAtLeast', {
count: minRows,
label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
})}
</Banner>
)}
{(rows.length === 0 && readOnly) && (
<Banner>
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
</Banner>
{!checkSkipValidation(value) && (
<React.Fragment>
{(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error">
{t('validation:requiresAtLeast', {
count: minRows,
label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
})}
</Banner>
)}
{(rows.length === 0 && readOnly) && (
<Banner>
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
</Banner>
)}
</React.Fragment>
)}
{provided.placeholder}
</div>

View File

@@ -0,0 +1,62 @@
import React from 'react';
import Check from '../../../icons/Check';
import Label from '../../Label';
import './index.scss';
const baseClass = 'custom-checkbox';
type CheckboxInputProps = {
onToggle: React.MouseEventHandler<HTMLButtonElement>
inputRef?: React.MutableRefObject<HTMLInputElement>
readOnly?: boolean
checked?: boolean
name?: string
id?: string
label?: string
required?: boolean
}
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
onToggle,
checked,
inputRef,
name,
id,
label,
readOnly,
required,
} = props;
return (
<span
className={[
baseClass,
checked && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ')}
>
<input
ref={inputRef}
id={id}
type="checkbox"
name={name}
checked={checked}
readOnly
/>
<button
type="button"
onClick={onToggle}
>
<span className={`${baseClass}__input`}>
<Check />
</span>
<Label
htmlFor={id}
label={label}
required={required}
/>
</button>
</span>
);
};

View File

@@ -14,6 +14,46 @@
&__error-wrap {
position: relative;
}
}
.custom-checkbox {
label {
padding-bottom: 0;
}
input {
// hidden HTML checkbox
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&__input {
// visible checkbox
@include formInput;
padding: 0;
line-height: 0;
position: relative;
width: $baseline;
height: $baseline;
margin-right: base(.5);
svg {
opacity: 0;
}
}
&--read-only {
.custom-checkbox__input {
background-color: var(--theme-elevation-100);
}
label {
color: var(--theme-elevation-400);
}
}
button {
@extend %btn-reset;
@@ -33,45 +73,13 @@
}
}
&__input {
@include formInput;
padding: 0;
line-height: 0;
position: relative;
width: $baseline;
height: $baseline;
margin-right: base(.5);
svg {
opacity: 0;
}
}
input {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&--checked {
button {
.checkbox__input {
.custom-checkbox__input {
svg {
opacity: 1;
}
}
}
}
&--read-only {
.checkbox__label {
color: var(--theme-elevation-400);
}
.checkbox__input {
background-color: var(--theme-elevation-100);
}
}
}

View File

@@ -4,10 +4,10 @@ import useField from '../../useField';
import withCondition from '../../withCondition';
import Error from '../../Error';
import { checkbox } from '../../../../../fields/validations';
import Check from '../../../icons/Check';
import FieldDescription from '../../FieldDescription';
import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import { CheckboxInput } from './Input';
import './index.scss';
@@ -52,6 +52,15 @@ const Checkbox: React.FC<Props> = (props) => {
condition,
});
const onToggle = useCallback(() => {
if (!readOnly) {
setValue(!value);
if (typeof onChange === 'function') onChange(!value);
}
}, [onChange, readOnly, setValue, value]);
const fieldID = `field-${path.replace(/\./gi, '__')}`;
return (
<div
className={[
@@ -73,27 +82,13 @@ const Checkbox: React.FC<Props> = (props) => {
message={errorMessage}
/>
</div>
<input
id={`field-${path.replace(/\./gi, '__')}`}
type="checkbox"
<CheckboxInput
onToggle={onToggle}
id={fieldID}
label={getTranslation(label || name, i18n)}
name={path}
checked={Boolean(value)}
readOnly
/>
<button
type="button"
onClick={readOnly ? undefined : () => {
setValue(!value);
if (typeof onChange === 'function') onChange(!value);
}}
>
<span className={`${baseClass}__input`}>
<Check />
</span>
<span className={`${baseClass}__label`}>
{getTranslation(label || name, i18n)}
</span>
</button>
<FieldDescription
value={value}
description={description}

View File

@@ -29,8 +29,8 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
const {
collection: collectionSlug,
depth,
locale,
fallbackLocale,
locale = null,
fallbackLocale = null,
data,
user,
overrideAccess = true,
@@ -44,10 +44,11 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
req.payloadAPI = 'local';
req.locale = locale || req?.locale || (payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null);
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
req.payload = payload;
req.i18n = i18n(payload.config.i18n);
req.files = {

View File

@@ -22,7 +22,7 @@ export default async function deleteLocal<T extends TypeWithID = any>(payload: P
collection: collectionSlug,
depth,
id,
locale = payload.config.localization ? payload.config.localization?.defaultLocale : null,
locale = null,
fallbackLocale = null,
user,
overrideAccess = true,
@@ -30,12 +30,13 @@ export default async function deleteLocal<T extends TypeWithID = any>(payload: P
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
const req = {
user,
payloadAPI: 'local',
locale,
fallbackLocale,
locale: locale ?? defaultLocale,
fallbackLocale: fallbackLocale ?? defaultLocale,
payload,
i18n: i18n(payload.config.i18n),
} as PayloadRequest;

View File

@@ -35,7 +35,7 @@ export default async function findLocal<T extends TypeWithID = any>(payload: Pay
page,
limit,
where,
locale = payload.config.localization ? payload.config.localization?.defaultLocale : null,
locale = null,
fallbackLocale = null,
user,
overrideAccess = true,
@@ -48,10 +48,11 @@ export default async function findLocal<T extends TypeWithID = any>(payload: Pay
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
req.payloadAPI = 'local';
req.locale = locale || req?.locale || (payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null);
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
req.i18n = i18n(payload.config.i18n);
req.payload = payload;

View File

@@ -28,8 +28,8 @@ export default async function findByIDLocal<T extends TypeWithID = any>(payload:
depth,
currentDepth,
id,
locale,
fallbackLocale,
locale = null,
fallbackLocale = null,
user,
overrideAccess = true,
disableErrors = false,
@@ -39,10 +39,11 @@ export default async function findByIDLocal<T extends TypeWithID = any>(payload:
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
req.payloadAPI = 'local';
req.locale = locale || req?.locale || (payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null);
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
req.i18n = i18n(payload.config.i18n);
req.payload = payload;

View File

@@ -24,7 +24,7 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
collection: collectionSlug,
depth,
id,
locale = payload.config.localization ? payload.config.localization?.defaultLocale : null,
locale = null,
fallbackLocale = null,
overrideAccess = true,
disableErrors = false,
@@ -33,10 +33,11 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
req.payloadAPI = 'local';
req.locale = locale || req?.locale || this?.config?.localization?.defaultLocale;
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
req.i18n = i18n(payload.config.i18n);
req.payload = payload;

View File

@@ -28,7 +28,7 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
page,
limit,
where,
locale = payload.config.localization ? payload.config.localization?.defaultLocale : null,
locale = null,
fallbackLocale = null,
user,
overrideAccess = true,
@@ -37,13 +37,14 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
} = options;
const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
const i18n = i18nInit(payload.config.i18n);
const req = {
user,
payloadAPI: 'local',
locale,
fallbackLocale,
locale: locale ?? defaultLocale,
fallbackLocale: fallbackLocale ?? defaultLocale,
payload,
i18n,
} as PayloadRequest;

View File

@@ -28,7 +28,7 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
const {
collection: collectionSlug,
depth,
locale = payload.config.localization ? payload.config.localization?.defaultLocale : null,
locale = null,
fallbackLocale = null,
data,
id,
@@ -44,12 +44,13 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
const collection = payload.collections[collectionSlug];
const i18n = i18nInit(payload.config.i18n);
const defaultLocale = payload.config.localization ? payload.config.localization?.defaultLocale : null;
const req = {
user,
payloadAPI: 'local',
locale,
fallbackLocale,
locale: locale ?? defaultLocale,
fallbackLocale: fallbackLocale ?? defaultLocale,
payload,
i18n,
files: {

View File

@@ -55,6 +55,7 @@ export const promise = async ({
if (hasLocalizedValue) {
let localizedValue = siblingDoc[field.name][req.locale];
if (typeof localizedValue === 'undefined' && req.fallbackLocale) localizedValue = siblingDoc[field.name][req.fallbackLocale];
if (localizedValue === null && (field.type === 'array' || field.type === 'blocks')) localizedValue = siblingDoc[field.name][req.fallbackLocale];
if (typeof localizedValue === 'undefined' && (field.type === 'group' || field.type === 'tab')) localizedValue = {};
if (typeof localizedValue === 'undefined') localizedValue = null;
siblingDoc[field.name] = localizedValue;

View File

@@ -50,7 +50,10 @@ export const promise = async ({
skipValidation,
}: Args): Promise<void> => {
const passesCondition = (field.admin?.condition) ? field.admin.condition(data, siblingData) : true;
const skipValidationFromHere = skipValidation || !passesCondition;
let skipValidationFromHere = skipValidation || !passesCondition;
const defaultLocale = req.payload.config?.localization ? req.payload.config.localization?.defaultLocale : 'en';
const operationLocale = req.locale || defaultLocale;
if (fieldAffectsData(field)) {
if (typeof siblingData[field.name] === 'undefined') {
@@ -73,6 +76,13 @@ export const promise = async ({
}
}
// skip validation if the field is localized and the incoming data is null
if (field.localized && operationLocale !== defaultLocale) {
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {
skipValidationFromHere = true;
}
}
// Execute hooks
if (field.hooks?.beforeChange) {
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
@@ -95,14 +105,12 @@ export const promise = async ({
// Validate
if (!skipValidationFromHere && field.validate) {
let valueToValidate;
let valueToValidate = siblingData[field.name];
let jsonError;
if (['array', 'blocks'].includes(field.type)) {
const rows = siblingData[field.name];
valueToValidate = Array.isArray(rows) ? rows.length : 0;
} else {
valueToValidate = siblingData[field.name];
}
@@ -138,21 +146,20 @@ export const promise = async ({
if (field.localized) {
mergeLocaleActions.push(() => {
if (req.payload.config.localization) {
const localeData = req.payload.config.localization.locales.reduce((locales, localeID) => {
let valueToSet = siblingData[field.name];
const localeData = req.payload.config.localization.locales.reduce((localizedValues, locale) => {
const fieldValue = locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale];
if (localeID !== req.locale) {
valueToSet = siblingDocWithLocales?.[field.name]?.[localeID];
}
if (typeof valueToSet !== 'undefined') {
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
return {
...locales,
[localeID]: valueToSet,
...localizedValues,
[locale]: fieldValue,
};
}
return locales;
return localizedValues;
}, {});
// If there are locales with data, set the data
@@ -232,7 +239,6 @@ export const promise = async ({
await Promise.all(promises);
}
break;
}
@@ -267,7 +273,6 @@ export const promise = async ({
await Promise.all(promises);
}
break;
}

View File

@@ -131,7 +131,7 @@ export const promise = async ({
case 'array':
case 'blocks': {
// Handle cases of arrays being intentionally set to 0
if (siblingData[field.name] === '0' || siblingData[field.name] === 0 || siblingData[field.name] === null) {
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) {
siblingData[field.name] = [];
}

View File

@@ -348,6 +348,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
array: (field: ArrayField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
default: undefined,
type: [buildSchema(
config,
field.fields,
@@ -410,7 +411,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
});
},
blocks: (field: BlockField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => {
const fieldSchema = [new Schema({}, { _id: false, discriminatorKey: 'blockType' })];
const fieldSchema = {
default: undefined,
type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })],
};
schema.add({
[field.name]: localizeSchema(field, fieldSchema, config.localization),

View File

@@ -169,6 +169,7 @@
"email": "E-mail",
"emailAddress": "E-mailová adresa",
"enterAValue": "Zadejte hodnotu",
"fallbackToDefaultLocale": "Zpětné přepnutí do výchozího locale",
"filter": "Filtr",
"filterWhere": "Filtrovat {{label}} kde",
"filters": "Filtry",

View File

@@ -170,6 +170,7 @@
"email": "E-Mail",
"emailAddress": "E-Mail-Adresse",
"enterAValue": "Gib einen Wert ein",
"fallbackToDefaultLocale": "Rückgriff auf das Standardgebietsschema",
"filter": "Filter",
"filterWhere": "Filter {{label}} wo",
"filters": "Filter",

View File

@@ -172,6 +172,7 @@
"email": "Email",
"emailAddress": "Email Address",
"enterAValue": "Enter a value",
"fallbackToDefaultLocale": "Fallback to default locale",
"filter": "Filter",
"filterWhere": "Filter {{label}} where",
"filters": "Filters",

View File

@@ -161,6 +161,7 @@
"email": "Correo electrónico",
"emailAddress": "Dirección de Correo Electrónico",
"enterAValue": "Introduce un valor",
"fallbackToDefaultLocale": "Volver a la configuración regional por defecto",
"filter": "Filtro",
"filterWhere": "Filtrar {{label}} donde",
"filters": "Filtros",

View File

@@ -162,6 +162,7 @@
"email": "E-mail",
"emailAddress": "Adresse e-mail",
"enterAValue": "Entrez une valeur",
"fallbackToDefaultLocale": "Retour à la locale par défaut",
"filter": "Filtrer",
"filterWhere": "Filtrer {{label}} où",
"filters": "Filtres",

View File

@@ -162,6 +162,7 @@
"email": "Email",
"emailAddress": "Indirizzo Email",
"enterAValue": "Inserisci un valore",
"fallbackToDefaultLocale": "Fallback al locale predefinito",
"filter": "Filtro",
"filterWhere": "Filtra {{label}} se",
"filters": "Filtri",

View File

@@ -161,6 +161,7 @@
"email": "メールアドレス",
"emailAddress": "メールアドレス",
"enterAValue": "値を入力",
"fallbackToDefaultLocale": "デフォルトロケールへのフォールバック",
"filter": "絞り込み",
"filterWhere": "{{label}} の絞り込み",
"filters": "絞り込み",

View File

@@ -161,6 +161,7 @@
"email": "အီးမေးလ်",
"emailAddress": "အီးမေးလ် လိပ်စာ",
"enterAValue": "တန်ဖိုးတစ်ခုထည့်ပါ။",
"fallbackToDefaultLocale": "မူရင်းဒေသသို့ ပြန်ပြောင်းပါ။",
"filter": "ဇကာ",
"filterWhere": "နေရာတွင် စစ်ထုတ်ပါ။",
"filters": "စစ်ထုတ်မှုများ",

View File

@@ -171,6 +171,7 @@
"email": "E-post",
"emailAddress": "E-postadresse",
"enterAValue": "Skriv inn en verdi",
"fallbackToDefaultLocale": "Tilbakestilling til standard lokalitet",
"filter": "Filtrer",
"filterWhere": "Filtrer {{label}} der",
"filters": "Filter",

View File

@@ -162,6 +162,7 @@
"email": "E-mail",
"emailAddress": "E-maildres",
"enterAValue": "Waarde invoeren",
"fallbackToDefaultLocale": "Terugval naar standaardtaal",
"filter": "Filter",
"filterWhere": "Filter {{label}} waar",
"filters": "Filters",

View File

@@ -161,6 +161,7 @@
"email": "Email",
"emailAddress": "Adres email",
"enterAValue": "Wpisz wartość",
"fallbackToDefaultLocale": "Powrót do domyślnego locale",
"filter": "Filtr",
"filterWhere": "Filtruj gdzie",
"filters": "Filtry",

View File

@@ -161,6 +161,7 @@
"email": "Email",
"emailAddress": "Endereço de Email",
"enterAValue": "Insira um valor",
"fallbackToDefaultLocale": "Recuo para o local padrão",
"filter": "Filtro",
"filterWhere": "Filtrar {{label}} em que",
"filters": "Filtros",

View File

@@ -171,6 +171,7 @@
"email": "Email",
"emailAddress": "Email",
"enterAValue": "Введите значение",
"fallbackToDefaultLocale": "Возврат к локали по умолчанию",
"filter": "Фильтр",
"filterWhere": "Где фильтровать",
"filters": "Фильтры",

View File

@@ -162,6 +162,7 @@
"email": "E-post",
"emailAddress": "E-postadress",
"enterAValue": "Ange ett värde",
"fallbackToDefaultLocale": "Återgång till standardlokalspråk",
"filter": "Filter",
"filterWhere": "Filtrera {{label}} där",
"filters": "Filter",

View File

@@ -172,6 +172,7 @@
"email": "อีเมล",
"emailAddress": "อีเมล",
"enterAValue": "ระบุค่า",
"fallbackToDefaultLocale": "สำรองไปยังตำแหน่งที่ตั้งเริ่มต้น",
"filter": "กรอง",
"filterWhere": "กรอง {{label}} เฉพาะ",
"filters": "กรอง",

View File

@@ -174,6 +174,7 @@
"editing": "Düzenleniyor",
"email": "E-posta",
"emailAddress": "E-posta adresi",
"fallbackToDefaultLocale": "Varsayılan yerel ayara geri dönme",
"filterWhere": "{{label}} filtrele:",
"filters": "Filtreler",
"globals": "Globaller",

View File

@@ -398,7 +398,7 @@
"addLabel": {
"type": "string"
},
"addLink":{
"addLink": {
"type": "string"
},
"addNew": {
@@ -647,6 +647,9 @@
"enterAValue": {
"type": "string"
},
"fallbackToDefaultLocale": {
"type": "string"
},
"filter": {
"type": "string"
},

View File

@@ -58,7 +58,7 @@
},
"error": {
"accountAlreadyActivated": "Цей аккаунт вже активований",
"autosaving": "Виникла проблема під час автозбереження цього документа.",
"autosaving": "Виникла проблема під час автозбереження цього документа.",
"correctInvalidFields": "Будь ласка, виправте невірні поля.",
"deletingFile": "Виникла помилка під час видалення файлу",
"deletingTitle": "Виникла помилка під час видалення {{title}}, Будь ласка, перевірте ваше з'єднання та спробуйте ще раз.",
@@ -168,6 +168,7 @@
"email": "Email",
"emailAddress": "Email адреса",
"enterAValue": "Введіть значення",
"fallbackToDefaultLocale": "Відновлення локалі за замовчуванням",
"filter": "Фільтрувати",
"filterWhere": "Де фільтрувати {{label}}",
"filters": "Фільтри",
@@ -303,4 +304,4 @@
"viewingVersions": "Огляд версій для {{entityLabel}} {{documentTitle}}",
"viewingVersionsGlobal": "Огляд версій для глобальної колекції {{entityLabel}}"
}
}
}

View File

@@ -162,6 +162,7 @@
"email": "Email",
"emailAddress": "Địa chỉ Email",
"enterAValue": "Nhập một giá trị",
"fallbackToDefaultLocale": "Dự phòng về ngôn ngữ mặc định",
"filter": "Lọc",
"filterWhere": "Lọc {{label}} với điều kiện:",
"filters": "Hiện bộ lọc",

View File

@@ -61,6 +61,7 @@ export default buildConfig({
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
fallback: true,
},
onInit: async (payload) => {
await payload.create({

View File

@@ -274,13 +274,13 @@ describe('Fields', () => {
});
});
it('should return empty array for arrays when no data present', async () => {
it('should return undefined arrays when no data present', async () => {
const document = await payload.create<ArrayField>({
collection: arrayFieldsSlug,
data: arrayDoc,
});
expect(document.potentiallyEmptyArray).toEqual([]);
expect(document.potentiallyEmptyArray).toBeUndefined();
});
it('should create with ids and nested ids', async () => {

View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from '../../../../src/collections/config/types';
export const arrayCollectionSlug = 'array-fields';
export const ArrayCollection: CollectionConfig = {
slug: arrayCollectionSlug,
fields: [
{
name: 'items',
type: 'array',
localized: true,
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
},
],
};

View File

@@ -1,6 +1,6 @@
import { buildConfig } from '../buildConfig';
import { devUser } from '../credentials';
import { GlobalArray } from './Array';
import { ArrayCollection } from './collections/Array';
import { LocalizedPost, RelationshipLocalized } from './payload-types';
import {
defaultLocale,
@@ -36,6 +36,7 @@ export default buildConfig({
localization: {
locales: [defaultLocale, spanishLocale],
defaultLocale,
fallback: true,
},
collections: [
{
@@ -65,6 +66,7 @@ export default buildConfig({
},
],
},
ArrayCollection,
{
slug: withRequiredLocalizedFields,
fields: [

View File

@@ -22,8 +22,9 @@ import {
spanishTitle,
} from './shared';
import type { Where } from '../../src/types';
import { arrayCollectionSlug } from './collections/Array';
const collection = config.collections[1]?.slug;
const collection = slug;
let serverURL;
@@ -199,6 +200,7 @@ describe('Localization', () => {
});
});
});
describe('Localized Relationship', () => {
let localizedRelation: LocalizedPost;
let localizedRelation2: LocalizedPost;
@@ -650,6 +652,73 @@ describe('Localization', () => {
expect(result.title[spanishLocale]).toStrictEqual(spanishTitle);
});
});
describe('Localized - Arrays', () => {
let docID;
beforeAll(async () => {
const englishDoc = await payload.create({
collection: arrayCollectionSlug,
data: {
items: [
{
text: englishTitle,
},
],
},
});
docID = englishDoc.id;
});
it('should use default locale as fallback', async () => {
const spanishDoc = await payload.findByID({
locale: spanishLocale,
collection: arrayCollectionSlug,
id: docID,
});
expect(spanishDoc.items[0].text).toStrictEqual(englishTitle);
});
it('should use empty array as value', async () => {
const updatedSpanishDoc = await payload.update({
collection: arrayCollectionSlug,
locale: spanishLocale,
id: docID,
data: {
items: [],
},
});
expect(updatedSpanishDoc.items).toStrictEqual([]);
});
it('should use fallback value if setting null', async () => {
await payload.update({
collection: arrayCollectionSlug,
locale: spanishLocale,
id: docID,
data: {
items: [],
},
});
const updatedSpanishDoc = await payload.update({
collection: arrayCollectionSlug,
locale: spanishLocale,
id: docID,
data: {
items: null,
},
});
// should return the value of the fallback locale
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(updatedSpanishDoc.items[0].text).toStrictEqual(englishTitle);
});
});
});
async function createLocalizedPost(data: {