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:
@@ -6,7 +6,7 @@ import './index.scss';
|
||||
|
||||
const baseClass = 'banner';
|
||||
|
||||
const Banner: React.FC<Props> = ({
|
||||
export const Banner: React.FC<Props> = ({
|
||||
children,
|
||||
className,
|
||||
to,
|
||||
|
||||
@@ -105,12 +105,17 @@ export const addFieldStatePromise = async ({
|
||||
await Promise.all(promises);
|
||||
|
||||
// Add values to field state
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add field to state
|
||||
state[`${path}${field.name}`] = fieldState;
|
||||
@@ -161,14 +166,18 @@ export const addFieldStatePromise = async ({
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
// Add values to field state
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add field to state
|
||||
state[`${path}${field.name}`] = fieldState;
|
||||
|
||||
59
src/admin/components/forms/NullifyField/index.tsx
Normal file
59
src/admin/components/forms/NullifyField/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,6 +340,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
@@ -335,10 +355,13 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -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,6 +375,9 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
@@ -372,6 +391,8 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
62
src/admin/components/forms/field-types/Checkbox/Input.tsx
Normal file
62
src/admin/components/forms/field-types/Checkbox/Input.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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] = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"email": "メールアドレス",
|
||||
"emailAddress": "メールアドレス",
|
||||
"enterAValue": "値を入力",
|
||||
"fallbackToDefaultLocale": "デフォルトロケールへのフォールバック",
|
||||
"filter": "絞り込み",
|
||||
"filterWhere": "{{label}} の絞り込み",
|
||||
"filters": "絞り込み",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"email": "အီးမေးလ်",
|
||||
"emailAddress": "အီးမေးလ် လိပ်စာ",
|
||||
"enterAValue": "တန်ဖိုးတစ်ခုထည့်ပါ။",
|
||||
"fallbackToDefaultLocale": "မူရင်းဒေသသို့ ပြန်ပြောင်းပါ။",
|
||||
"filter": "ဇကာ",
|
||||
"filterWhere": "နေရာတွင် စစ်ထုတ်ပါ။",
|
||||
"filters": "စစ်ထုတ်မှုများ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"email": "Email",
|
||||
"emailAddress": "Email",
|
||||
"enterAValue": "Введите значение",
|
||||
"fallbackToDefaultLocale": "Возврат к локали по умолчанию",
|
||||
"filter": "Фильтр",
|
||||
"filterWhere": "Где фильтровать",
|
||||
"filters": "Фильтры",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -172,6 +172,7 @@
|
||||
"email": "อีเมล",
|
||||
"emailAddress": "อีเมล",
|
||||
"enterAValue": "ระบุค่า",
|
||||
"fallbackToDefaultLocale": "สำรองไปยังตำแหน่งที่ตั้งเริ่มต้น",
|
||||
"filter": "กรอง",
|
||||
"filterWhere": "กรอง {{label}} เฉพาะ",
|
||||
"filters": "กรอง",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"email": "Email",
|
||||
"emailAddress": "Email адреса",
|
||||
"enterAValue": "Введіть значення",
|
||||
"fallbackToDefaultLocale": "Відновлення локалі за замовчуванням",
|
||||
"filter": "Фільтрувати",
|
||||
"filterWhere": "Де фільтрувати {{label}}",
|
||||
"filters": "Фільтри",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -61,6 +61,7 @@ export default buildConfig({
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
fallback: true,
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
21
test/localization/collections/Array/index.ts
Normal file
21
test/localization/collections/Array/index.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user