fix: i18n duplicate field ui replication (#582)

This commit is contained in:
Daniel Lautzenheiser 2023-02-24 09:01:59 -05:00 committed by GitHub
parent 142bbdda13
commit 0b451b1db7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 168 additions and 55 deletions

View File

@ -21,6 +21,7 @@ import { borders, colors, lengths, transitions } from '@staticcms/core/component
import { transientOptions } from '@staticcms/core/lib';
import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { isFieldDuplicate, isFieldHidden } from '@staticcms/core/lib/i18n';
import { resolveWidget } from '@staticcms/core/lib/registry';
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
@ -144,10 +145,11 @@ const EditorControl = ({
fieldsErrors,
submitted,
getAsset,
isDisabled,
isFieldDuplicate,
isFieldHidden,
isHidden = false,
isDisabled = false,
isParentDuplicate = false,
isFieldDuplicate: deprecatedIsFieldDuplicate,
isParentHidden = false,
isFieldHidden: deprecatedIsFieldHidden,
locale,
mediaPaths,
openMediaLibrary,
@ -191,6 +193,15 @@ const EditorControl = ({
[collection],
);
const isDuplicate = useMemo(
() => isParentDuplicate || isFieldDuplicate(field, locale, i18n?.defaultLocale),
[field, i18n?.defaultLocale, isParentDuplicate, locale],
);
const isHidden = useMemo(
() => isParentHidden || isFieldHidden(field, locale, i18n?.defaultLocale),
[field, i18n?.defaultLocale, isParentHidden, locale],
);
useEffect(() => {
if ((!dirty && !submitted) || isHidden) {
return;
@ -257,9 +268,11 @@ const EditorControl = ({
fieldsErrors,
submitted,
getAsset: handleGetAsset,
isDisabled: isDisabled ?? false,
isFieldDuplicate,
isFieldHidden,
isDisabled: isDisabled || isDuplicate,
isDuplicate,
isFieldDuplicate: deprecatedIsFieldDuplicate,
isHidden,
isFieldHidden: deprecatedIsFieldHidden,
label: getFieldLabel(field, t),
locale,
mediaPaths,
@ -330,9 +343,16 @@ interface EditorControlOwnProps {
fieldsErrors: FieldsErrors;
submitted: boolean;
isDisabled?: boolean;
isParentDuplicate?: boolean;
/**
* @deprecated use isDuplicate instead
*/
isFieldDuplicate?: (field: Field) => boolean;
isParentHidden?: boolean;
/**
* @deprecated use isHidden instead
*/
isFieldHidden?: (field: Field) => boolean;
isHidden?: boolean;
locale?: string;
parentPath: string;
value: ValueOrNestedValue;

View File

@ -195,8 +195,6 @@ const EditorControlPane = ({
) : null}
{fields.map(field => {
const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
const isDuplicate = isFieldDuplicate(field, locale, i18n?.defaultLocale);
const isHidden = isFieldHidden(field, locale, i18n?.defaultLocale);
const key = i18n ? `field-${locale}_${field.name}` : `field-${field.name}`;
return (
@ -206,8 +204,6 @@ const EditorControlPane = ({
value={getFieldValue(field, entry, isTranslatable, locale)}
fieldsErrors={fieldsErrors}
submitted={submitted}
isDisabled={isDuplicate}
isHidden={isHidden}
isFieldDuplicate={field => isFieldDuplicate(field, locale, i18n?.defaultLocale)}
isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
locale={locale}

View File

@ -235,7 +235,7 @@ export interface DisplayURLState {
export type TranslatedProps<T> = T & ReactPolyglotTranslateProps;
/**
* @deprecated Should use `useMediaAsset` React hook instead
* @deprecated Use `useMediaAsset` React hook instead. Will be removed in v2.0.0
*/
export type GetAssetFunction<F extends BaseField = UnknownField> = (
path: string,
@ -251,11 +251,19 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField> {
submitted: boolean;
forList: boolean;
/**
* @deprecated Should use `useMediaAsset` React hook instead
* @deprecated Use `useMediaAsset` React hook instead. Will be removed in v2.0.0
*/
getAsset: GetAssetFunction<F>;
isDisabled: boolean;
isDuplicate: boolean;
/**
* @deprecated Use `isDuplicate` instead. Will be removed in v2.0.0
*/
isFieldDuplicate: EditorControlProps['isFieldDuplicate'];
isHidden: boolean;
/**
* @deprecated Use `isHidden` instead. Will be removed in v2.0.0
*/
isFieldHidden: EditorControlProps['isFieldHidden'];
label: string;
locale: string | undefined;

View File

@ -38,7 +38,9 @@ const createControlWrapper = <V = unknown, F extends BaseField = UnknownField>({
return Promise.resolve(null) as any;
},
isDisabled = false,
isDuplicate = false,
isFieldDuplicate = () => false,
isHidden = false,
isFieldHidden = () => false,
label = defaultLabel,
locale = 'en',
@ -75,7 +77,9 @@ const createControlWrapper = <V = unknown, F extends BaseField = UnknownField>({
forList={forList}
getAsset={getAsset}
isDisabled={isDisabled}
isDuplicate={isDuplicate}
isFieldDuplicate={isFieldDuplicate}
isHidden={isHidden}
isFieldHidden={isFieldHidden}
label={label}
locale={locale}

View File

@ -1,7 +1,7 @@
import { red } from '@mui/material/colors';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { BooleanField, WidgetControlProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC } from 'react';
@ -9,10 +9,15 @@ import type { ChangeEvent, FC } from 'react';
const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
value,
label,
isDuplicate,
onChange,
hasErrors,
}) => {
const [internalValue, setInternalValue] = useState(value);
const [internalRawValue, setInternalValue] = useState(value);
const internalValue = useMemo(
() => (isDuplicate ? value : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {

View File

@ -60,6 +60,7 @@ function valueToOption(val: string | { name: string; label?: string }): {
const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, CodeField>> = ({
field,
isDuplicate,
onChange,
hasErrors,
value,
@ -77,7 +78,12 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
const valueIsMap = useMemo(() => Boolean(!field.output_code_only), [field.output_code_only]);
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const [lang, setLang] = useState<ProcessedCodeLanguage | null>(null);
const [collapsed, setCollapsed] = useState(false);

View File

@ -3,7 +3,7 @@ import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import { styled } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { ChromePicker } from 'react-color';
import validateColor from 'validate-color';
@ -104,6 +104,7 @@ const ClickOutsideDiv = styled('div')`
const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
field,
isDuplicate,
onChange,
value,
hasErrors,
@ -116,7 +117,11 @@ const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
}, [collapsed]);
const [showColorPicker, setShowColorPicker] = useState(false);
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
// show/hide color picker
const handleClick = useCallback(() => {

View File

@ -62,6 +62,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
value,
t,
isDisabled,
isDuplicate,
onChange,
hasErrors,
}) => {
@ -118,7 +119,11 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
: field.default;
}, [field.default, field.picker_utc, format, inputFormat, timezoneOffset]);
const [internalValue, setInternalValue] = useState(value);
const [internalRawValue, setInternalValue] = useState(value);
const internalValue = useMemo(
() => (isDuplicate ? value : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const dateValue: Date = useMemo(() => {
let valueToParse = internalValue;

View File

@ -213,6 +213,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
collection,
field,
entry,
isDuplicate,
onChange,
openMediaLibrary,
clearMediaControl,
@ -222,7 +223,11 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
}) => {
const controlID = useUUID();
const [collapsed, setCollapsed] = useState(false);
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const handleOnChange = useCallback(
(newValue: string | string[]) => {

View File

@ -68,7 +68,9 @@ interface SortableItemProps {
field: ListField;
fieldsErrors: FieldsErrors;
submitted: boolean;
isDuplicate: boolean;
isFieldDuplicate: ((field: Field<UnknownField>) => boolean) | undefined;
isHidden: boolean;
isFieldHidden: ((field: Field<UnknownField>) => boolean) | undefined;
locale: string | undefined;
path: string;
@ -86,7 +88,9 @@ const SortableItem: FC<SortableItemProps> = ({
field,
fieldsErrors,
submitted,
isDuplicate,
isFieldDuplicate,
isHidden,
isFieldHidden,
locale,
path,
@ -118,7 +122,9 @@ const SortableItem: FC<SortableItemProps> = ({
field={field}
fieldsErrors={fieldsErrors}
submitted={submitted}
isDuplicate={isDuplicate}
isFieldDuplicate={isFieldDuplicate}
isHidden={isHidden}
isFieldHidden={isFieldHidden}
locale={locale}
path={path}
@ -188,7 +194,9 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
field,
fieldsErrors,
submitted,
isDuplicate,
isFieldDuplicate,
isHidden,
isFieldHidden,
locale,
onChange,
@ -356,7 +364,9 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
field={field}
fieldsErrors={fieldsErrors}
submitted={submitted}
isDuplicate={isDuplicate}
isFieldDuplicate={isFieldDuplicate}
isHidden={isHidden}
isFieldHidden={isFieldHidden}
locale={locale}
path={path}

View File

@ -93,7 +93,9 @@ interface ListItemProps
| 'field'
| 'fieldsErrors'
| 'submitted'
| 'isDuplicate'
| 'isFieldDuplicate'
| 'isHidden'
| 'isFieldHidden'
| 'locale'
| 'path'
@ -114,7 +116,9 @@ const ListItem: FC<ListItemProps> = ({
field,
fieldsErrors,
submitted,
isDuplicate,
isFieldDuplicate,
isHidden,
isFieldHidden,
locale,
path,
@ -197,9 +201,6 @@ const ListItem: FC<ListItemProps> = ({
[collapsed],
);
const isDuplicate = isFieldDuplicate && isFieldDuplicate(field);
const isHidden = isFieldHidden && isFieldHidden(field);
const finalValue = useMemo(() => {
if (field.fields && field.fields.length === 1) {
return {
@ -232,8 +233,9 @@ const ListItem: FC<ListItemProps> = ({
submitted={submitted}
parentPath={path}
isDisabled={isDuplicate}
isHidden={isHidden}
isParentDuplicate={isDuplicate}
isFieldDuplicate={isFieldDuplicate}
isParentHidden={isHidden}
isFieldHidden={isFieldHidden}
locale={locale}
i18n={i18n}

View File

@ -39,9 +39,14 @@ export interface WithMarkdownControlProps {
const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = controlProps => {
const { label, value, onChange, hasErrors, collection, entry, field } = controlProps;
const { label, value, isDuplicate, onChange, hasErrors, collection, entry, field } =
controlProps;
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const [hasFocus, setHasFocus] = useState(false);
const debouncedFocus = useDebounce(hasFocus, 150);

View File

@ -1,5 +1,5 @@
import TextField from '@mui/material/TextField';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { FieldError, NumberField, WidgetControlProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC } from 'react';
@ -62,10 +62,15 @@ const NumberControl: FC<WidgetControlProps<string | number, NumberField>> = ({
label,
field,
value,
isDuplicate,
onChange,
hasErrors,
}) => {
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View File

@ -53,7 +53,9 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
fieldsErrors,
submitted,
forList,
isDuplicate,
isFieldDuplicate,
isHidden,
isFieldHidden,
locale,
path,
@ -96,9 +98,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
parentPath = splitPath.join('.');
}
const isDuplicate = isFieldDuplicate && isFieldDuplicate(field);
const isHidden = isFieldHidden && isFieldHidden(field);
return (
<EditorControl
key={index}
@ -109,8 +108,9 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
submitted={submitted}
parentPath={parentPath}
isDisabled={isDuplicate}
isHidden={isHidden}
isParentDuplicate={isDuplicate}
isFieldDuplicate={isFieldDuplicate}
isParentHidden={isHidden}
isFieldHidden={isFieldHidden}
locale={locale}
i18n={i18n}
@ -122,8 +122,10 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
fieldsErrors,
forList,
i18n,
isDuplicate,
isFieldDuplicate,
isFieldHidden,
isHidden,
locale,
multiFields,
path,

View File

@ -118,13 +118,18 @@ function getSelectedValue(
const RelationControl: FC<WidgetControlProps<string | string[], RelationField>> = ({
value,
field,
isDuplicate,
onChange,
config,
locale,
label,
hasErrors,
}) => {
const [internalValue, setInternalValue] = useState(value);
const [internalRawValue, setInternalValue] = useState(value);
const internalValue = useMemo(
() => (isDuplicate ? value : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const [initialOptions, setInitialOptions] = useState<HitOption[]>([]);
const searchCollectionSelector = useMemo(

View File

@ -31,9 +31,14 @@ const SelectControl: FC<WidgetControlProps<string | number | (string | number)[]
field,
value,
hasErrors,
isDuplicate,
onChange,
}) => {
const [internalValue, setInternalValue] = useState(value);
const [internalRawValue, setInternalValue] = useState(value);
const internalValue = useMemo(
() => (isDuplicate ? value : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const fieldOptions: (string | number | Option)[] = useMemo(() => field.options, [field.options]);
const isMultiple = useMemo(() => field.multiple ?? false, [field.multiple]);

View File

@ -1,5 +1,5 @@
import TextField from '@mui/material/TextField';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC } from 'react';
@ -7,10 +7,15 @@ import type { ChangeEvent, FC } from 'react';
const StringControl: FC<WidgetControlProps<string, StringOrTextField>> = ({
value,
label,
isDuplicate,
onChange,
hasErrors,
}) => {
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View File

@ -1,5 +1,5 @@
import TextField from '@mui/material/TextField';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC } from 'react';
@ -7,10 +7,15 @@ import type { ChangeEvent, FC } from 'react';
const TextControl: FC<WidgetControlProps<string, StringOrTextField>> = ({
label,
value,
isDuplicate,
onChange,
hasErrors,
}) => {
const [internalValue, setInternalValue] = useState(value ?? '');
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (isDuplicate ? value ?? '' : internalRawValue),
[internalRawValue, isDuplicate, value],
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View File

@ -55,10 +55,12 @@ The react component that renders the control. It receives the following props:
| fieldsErrors | object | Key/value object of field names mapping to validation errors |
| isDisabled | boolean | Specifies if the widget control should be disabled |
| submitted | boolean | Specifies if a save attempt has been made in the editor session |
| forList | boolean | Specifices if the widget is within a `list` widget |
| isFieldDuplicate | function | Function that given a field configuration, returns if that field is a duplicate |
| isFieldHidden | function | Function that given a field configuration, returns if that field is hidden |
| getAsset | Async function | __Deprecated__ Function that given a url returns (as a promise) a loaded asset |
| forList | boolean | Specifies if the widget is within a `list` widget |
| isDuplicate | function | Specifies if that field is an i18n duplicate |
| isFieldDuplicate | function | **Deprecated** Function that given a field configuration, returns if that field is a duplicate |
| isHidden | function | Specifies if that field should be hidden |
| isFieldHidden | function | **Deprecated** Function that given a field configuration, returns if that field is hidden |
| getAsset | Async function | **Deprecated** Function that given a url returns (as a promise) a loaded asset |
| locale | string<br />\| undefined | The current locale of the editor |
| mediaPaths | object | Key/value object of control IDs (passed to the media library) mapping to media paths |
| clearMediaControl | function | Clears a control ID's value from the internal store |
@ -107,7 +109,7 @@ The react component that renders the preview. It receives the following props:
| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) |
| config | object | The current Static CMS config. See [configuration options](/docs/configuration-options) |
| entry | object | Object with a `data` field that contains the current value of all widgets in the editor |
| getAsset | Async function | __Deprecated__ Function that given a url returns (as a promise) a loaded asset |
| getAsset | Async function | **Deprecated** Function that given a url returns (as a promise) a loaded asset |
### Options
@ -167,9 +169,12 @@ import CMS from '@staticcms/core';
const CategoriesControl = ({ label, value, field, onChange }) => {
const separator = useMemo(() => field.separator ?? ', ', [field.separator]);
const handleChange = useCallback((e) => {
const handleChange = useCallback(
e => {
onChange(e.target.value.split(separator).map(e => e.trim()));
}, [separator, onChange]);
},
[separator, onChange],
);
return (
<div>
@ -178,7 +183,8 @@ const CategoriesControl = ({ label, value, field, onChange }) => {
id="inputId"
type="text"
value={value ? value.join(separator) : ''}
onChange={handleChange} />
onChange={handleChange}
/>
</div>
);
};
@ -208,15 +214,23 @@ import CMS from '@staticcms/core';
import type { WidgetControlProps, WidgetPreviewProps } from '@staticcms/core';
interface CategoriesField {
widget: 'categories'
widget: 'categories';
}
const CategoriesControl = ({ label, value, field, onChange }: WidgetControlProps<string[], CategoriesField>) => {
const CategoriesControl = ({
label,
value,
field,
onChange,
}: WidgetControlProps<string[], CategoriesField>) => {
const separator = useMemo(() => field.separator ?? ', ', [field.separator]);
const handleChange = useCallback((e) => {
const handleChange = useCallback(
e => {
onChange(e.target.value.split(separator).map(e => e.trim()));
}, [separator, onChange]);
},
[separator, onChange],
);
return (
<div>
@ -225,7 +239,8 @@ const CategoriesControl = ({ label, value, field, onChange }: WidgetControlProps
id="inputId"
type="text"
value={value ? value.join(separator) : ''}
onChange={handleChange} />
onChange={handleChange}
/>
</div>
);
};
@ -367,7 +382,7 @@ const FileControl = ({ collection, field, value, entry, onChange }) => {
return [
h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
h('img', { role: 'presentation', src: assetSource })
h('img', { role: 'presentation', src: assetSource }),
];
};
```