feat: dependent fields (#839)

This commit is contained in:
Daniel Lautzenheiser 2023-06-13 15:13:18 -04:00 committed by GitHub
parent 0dc7576bb4
commit 654263dd6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 236 additions and 65 deletions

2
BREAKING_CHANGES.md Normal file
View File

@ -0,0 +1,2 @@
- gitea API config has changed.
- hidden removed as widget parameter

View File

@ -87,14 +87,35 @@ collections:
- label: Question - label: Question
name: title name: title
widget: string widget: string
- label: Type
name: type
widget: select
default: internal
options:
- label: Internal
value: internal
- label: External
value: external
- label: URL
name: url
widget: string
condition:
field: type
value: external
- label: Answer - label: Answer
name: body name: body
widget: markdown widget: markdown
condition:
field: type
value: internal
- name: posts - name: posts
label: Posts label: Posts
label_singular: Post label_singular: Post
widget: list widget: list
summary: "{{fields.post | split('|', '$1')}}" summary: "{{fields.post | split('|', '$1')}}"
condition:
field: type
value: internal
fields: fields:
- label: Related Post - label: Related Post
name: post name: post
@ -870,7 +891,7 @@ collections:
- value: 2 - value: 2
label: Another fancy label label: Another fancy label
- value: c - value: c
label: And one more fancy label test test test test test test test label: And one more fancy label
- label: Value and Label With Default - label: Value and Label With Default
name: value_and_label_with_default name: value_and_label_with_default
widget: select widget: select

View File

@ -1,4 +1,4 @@
import SwipeableDrawer from '@mui/material/SwipeableDrawer'; import Drawer from '@mui/material/Drawer';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import FilterControl from '../FilterControl'; import FilterControl from '../FilterControl';
@ -38,24 +38,16 @@ const MobileCollectionControlsDrawer = ({
fields, fields,
onSortClick, onSortClick,
}: MobileCollectionControlsDrawerProps) => { }: MobileCollectionControlsDrawerProps) => {
const iOS = useMemo(
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
[],
);
const container = useMemo( const container = useMemo(
() => (typeof window !== 'undefined' ? window.document.body : undefined), () => (typeof window !== 'undefined' ? window.document.body : undefined),
[], [],
); );
return ( return (
<SwipeableDrawer <Drawer
disableBackdropTransition={!iOS}
disableDiscovery={iOS}
container={container} container={container}
variant="temporary" variant="temporary"
open={mobileOpen} open={mobileOpen}
onOpen={onMobileOpenToggle}
onClose={onMobileOpenToggle} onClose={onMobileOpenToggle}
anchor="right" anchor="right"
ModalProps={{ ModalProps={{
@ -110,7 +102,7 @@ const MobileCollectionControlsDrawer = ({
/> />
) : null} ) : null}
</div> </div>
</SwipeableDrawer> </Drawer>
); );
}; };

View File

@ -17,8 +17,9 @@ import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
import useUUID from '@staticcms/core/lib/hooks/useUUID'; import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { isFieldDuplicate, isFieldHidden } from '@staticcms/core/lib/i18n'; import { isFieldDuplicate, isFieldHidden } from '@staticcms/core/lib/i18n';
import { resolveWidget } from '@staticcms/core/lib/registry'; import { resolveWidget } from '@staticcms/core/lib/registry';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { fileForEntry } from '@staticcms/core/lib/util/collection.util'; import { fileForEntry } from '@staticcms/core/lib/util/collection.util';
import { getFieldLabel } from '@staticcms/core/lib/util/field.util'; import { getFieldLabel, useHidden } from '@staticcms/core/lib/util/field.util';
import { isNotNullish } from '@staticcms/core/lib/util/null.util'; import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import { validate } from '@staticcms/core/lib/util/validation.util'; import { validate } from '@staticcms/core/lib/util/validation.util';
import { selectFieldErrors } from '@staticcms/core/reducers/selectors/entryDraft'; import { selectFieldErrors } from '@staticcms/core/reducers/selectors/entryDraft';
@ -49,7 +50,6 @@ const EditorControl = ({
submitted, submitted,
disabled = false, disabled = false,
parentDuplicate = false, parentDuplicate = false,
parentHidden = false,
locale, locale,
mediaPaths, mediaPaths,
openMediaLibrary, openMediaLibrary,
@ -95,13 +95,19 @@ const EditorControl = ({
() => parentDuplicate || isFieldDuplicate(field, locale, i18n?.defaultLocale), () => parentDuplicate || isFieldDuplicate(field, locale, i18n?.defaultLocale),
[field, i18n?.defaultLocale, parentDuplicate, locale], [field, i18n?.defaultLocale, parentDuplicate, locale],
); );
const hidden = useMemo( const i18nDisabled = useMemo(
() => parentHidden || isFieldHidden(field, locale, i18n?.defaultLocale), () => isFieldHidden(field, locale, i18n?.defaultLocale),
[field, i18n?.defaultLocale, parentHidden, locale], [field, i18n?.defaultLocale, locale],
); );
const hidden = useHidden(field, entry);
useEffect(() => { useEffect(() => {
if ((!dirty && !submitted) || hidden || disabled) { if (hidden) {
dispatch(changeDraftFieldValidation(path, [], i18n, isMeta));
return;
}
if ((!dirty && !submitted) || disabled || i18nDisabled) {
return; return;
} }
@ -111,7 +117,21 @@ const EditorControl = ({
}; };
validateValue(); validateValue();
}, [dirty, dispatch, field, i18n, hidden, path, submitted, t, value, widget, disabled, isMeta]); }, [
dirty,
dispatch,
field,
i18n,
hidden,
path,
submitted,
t,
value,
widget,
disabled,
isMeta,
i18nDisabled,
]);
const handleChangeDraftField = useCallback( const handleChangeDraftField = useCallback(
(value: ValueOrNestedValue) => { (value: ValueOrNestedValue) => {
@ -157,7 +177,7 @@ const EditorControl = ({
} }
return ( return (
<div> <div className={classNames(hidden && 'hidden')}>
{createElement(widget.control, { {createElement(widget.control, {
key: `${id}-${version}`, key: `${id}-${version}`,
collection, collection,
@ -167,9 +187,8 @@ const EditorControl = ({
field: field as UnknownField, field: field as UnknownField,
fieldsErrors, fieldsErrors,
submitted, submitted,
disabled: disabled || duplicate || hidden, disabled: disabled || duplicate || hidden || i18nDisabled,
duplicate, duplicate,
hidden,
label: getFieldLabel(field, t), label: getFieldLabel(field, t),
locale, locale,
mediaPaths, mediaPaths,
@ -226,7 +245,6 @@ interface EditorControlOwnProps {
submitted: boolean; submitted: boolean;
disabled?: boolean; disabled?: boolean;
parentDuplicate?: boolean; parentDuplicate?: boolean;
parentHidden?: boolean;
locale?: string; locale?: string;
parentPath: string; parentPath: string;
value: ValueOrNestedValue; value: ValueOrNestedValue;

View File

@ -56,6 +56,7 @@ const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) =>
flex flex
items-center items-center
gap-1 gap-1
pl-1
" "
> >
{breadcrumbs.map((breadcrumb, index) => {breadcrumbs.map((breadcrumb, index) =>
@ -73,6 +74,7 @@ const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) =>
whitespace-nowrap whitespace-nowrap
focus:outline-none focus:outline-none
focus:ring-4 focus:ring-4
rounded-md
focus:ring-gray-200 focus:ring-gray-200
dark:focus:ring-slate-700 dark:focus:ring-slate-700
`, `,

View File

@ -71,7 +71,7 @@ const Navbar = ({
inEditor && 'pl-3 md:pl-0', inEditor && 'pl-3 md:pl-0',
)} )}
> >
<div className="flex flex-1 h-full items-stretch justify-start gap-2 md:gap-4 truncate"> <div className="flex flex-1 h-full items-stretch justify-start gap-2 md:gap-3 truncate">
<div <div
className={classNames( className={classNames(
` `

View File

@ -1,4 +1,4 @@
import SwipeableDrawer from '@mui/material/SwipeableDrawer'; import Drawer from '@mui/material/Drawer';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import SidebarContent from './SidebarContent'; import SidebarContent from './SidebarContent';
@ -11,24 +11,16 @@ interface NavigationDrawerProps {
} }
const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => { const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => {
const iOS = useMemo(
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
[],
);
const container = useMemo( const container = useMemo(
() => (typeof window !== 'undefined' ? window.document.body : undefined), () => (typeof window !== 'undefined' ? window.document.body : undefined),
[], [],
); );
return ( return (
<SwipeableDrawer <Drawer
disableBackdropTransition={!iOS}
disableDiscovery={iOS}
container={container} container={container}
variant="temporary" variant="temporary"
open={mobileOpen} open={mobileOpen}
onOpen={onMobileOpenToggle}
onClose={onMobileOpenToggle} onClose={onMobileOpenToggle}
ModalProps={{ ModalProps={{
keepMounted: true, // Better open performance on mobile. keepMounted: true, // Better open performance on mobile.
@ -49,7 +41,7 @@ const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerPr
<div onClick={onMobileOpenToggle} className="w-full h-full"> <div onClick={onMobileOpenToggle} className="w-full h-full">
<SidebarContent /> <SidebarContent />
</div> </div>
</SwipeableDrawer> </Drawer>
); );
}; };

View File

@ -179,7 +179,7 @@ export interface FieldPatternFilterRule extends BaseFieldFilterRule {
} }
export interface FieldValueFilterRule extends BaseFieldFilterRule { export interface FieldValueFilterRule extends BaseFieldFilterRule {
value: string | string[]; value: string | number | boolean | (string | number | boolean)[];
matchAll?: boolean; matchAll?: boolean;
} }
@ -305,7 +305,6 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField, EV =
forSingleList: boolean; forSingleList: boolean;
disabled: boolean; disabled: boolean;
duplicate: boolean; duplicate: boolean;
hidden: boolean;
label: string; label: string;
locale: string | undefined; locale: string | undefined;
// @deprecated Use useMediaInsert instead // @deprecated Use useMediaInsert instead
@ -603,6 +602,7 @@ export interface BaseField {
i18n?: boolean | 'translate' | 'duplicate' | 'none'; i18n?: boolean | 'translate' | 'duplicate' | 'none';
comment?: string; comment?: string;
widget: string; widget: string;
condition?: FieldFilterRule | FieldFilterRule[];
} }
export interface MediaField extends BaseField { export interface MediaField extends BaseField {

View File

@ -0,0 +1,100 @@
import { createMockEntry } from '@staticcms/test/data/entry.mock';
import { isHidden } from '../field.util';
import { I18N_FIELD_NONE } from '../../i18n';
import type { StringOrTextField } from '@staticcms/core/interface';
describe('filterEntries', () => {
const mockTitleField: StringOrTextField = {
label: 'Title',
name: 'title',
widget: 'string',
};
const mockUrlField: StringOrTextField = {
label: 'URL',
name: 'url',
widget: 'string',
i18n: I18N_FIELD_NONE,
condition: {
field: 'type',
value: 'external',
},
};
const mockBodyField: StringOrTextField = {
label: 'Body',
name: 'body',
widget: 'text',
condition: [
{
field: 'type',
value: 'internal',
},
{
field: 'hasSummary',
value: true,
},
],
};
const mockExternalEntry = createMockEntry({
path: 'path/to/file-1.md',
data: {
title: 'I am a title',
type: 'external',
url: 'http://example.com',
hasSummary: false,
},
});
const mockInternalEntry = createMockEntry({
path: 'path/to/file-1.md',
data: {
title: 'I am a title',
type: 'internal',
body: 'I am the body of your post',
hasSummary: false,
},
});
const mockHasSummaryEntry = createMockEntry({
path: 'path/to/file-1.md',
data: {
title: 'I am a title',
type: 'external',
url: 'http://example.com',
body: 'I am the body of your post',
hasSummary: true,
},
});
describe('isHidden', () => {
it('should show field by default', () => {
expect(isHidden(mockTitleField, mockExternalEntry)).toBeFalsy();
});
it('should hide field if single condition is not met', () => {
expect(isHidden(mockUrlField, mockInternalEntry)).toBeTruthy();
});
it('should show field if single condition is met', () => {
expect(isHidden(mockUrlField, mockExternalEntry)).toBeFalsy();
});
it('should hide field if all multiple conditions are not met', () => {
expect(isHidden(mockBodyField, mockExternalEntry)).toBeTruthy();
});
it('should show field if single condition is met', () => {
expect(isHidden(mockBodyField, mockHasSummaryEntry)).toBeFalsy();
expect(isHidden(mockBodyField, mockInternalEntry)).toBeFalsy();
});
it('should show field if entry is undefined', () => {
expect(isHidden(mockTitleField, undefined)).toBeFalsy();
expect(isHidden(mockUrlField, undefined)).toBeFalsy();
expect(isHidden(mockBodyField, undefined)).toBeFalsy();
});
});
});

View File

@ -88,7 +88,7 @@ describe('filterEntries', () => {
mockTags1and4Entry, mockTags1and4Entry,
]); ]);
expect(filterEntries(entries, { field: 'draft', value: 'true' })).toEqual([ expect(filterEntries(entries, { field: 'draft', value: true })).toEqual([
mockEnglishEntry, mockEnglishEntry,
mockTags1and4Entry, mockTags1and4Entry,
]); ]);
@ -119,7 +119,7 @@ describe('filterEntries', () => {
mockTags1and4Entry, mockTags1and4Entry,
]); ]);
expect(filterEntries(entries, { field: 'numbers', value: '8' })).toEqual([ expect(filterEntries(entries, { field: 'numbers', value: 8 })).toEqual([
mockRandomFileNameEntry, mockRandomFileNameEntry,
mockTags1and4Entry, mockTags1and4Entry,
]); ]);

View File

@ -1,7 +1,9 @@
import get from 'lodash/get'; import get from 'lodash/get';
import { useMemo } from 'react';
import { getLocaleDataPath } from '../i18n'; import { getLocaleDataPath } from '../i18n';
import { keyToPathArray } from '../widgets/stringTemplate'; import { keyToPathArray } from '../widgets/stringTemplate';
import { entryMatchesFieldRule } from './filter.util';
import type { import type {
BaseField, BaseField,
@ -88,3 +90,23 @@ export function getFieldValue(
return entry.data?.[field.name]; return entry.data?.[field.name];
} }
export function isHidden(field: Field, entry: Entry | undefined): boolean {
if (field.condition) {
if (!entry) {
return false;
}
if (Array.isArray(field.condition)) {
return !field.condition.find(r => entryMatchesFieldRule(entry, r));
}
return !entryMatchesFieldRule(entry, field.condition);
}
return false;
}
export function useHidden(field: Field, entry: Entry | undefined): boolean {
return useMemo(() => isHidden(field, entry), [entry, field]);
}

View File

@ -2,7 +2,7 @@ import { parse } from 'path';
import type { Entry, FieldFilterRule, FilterRule } from '@staticcms/core/interface'; import type { Entry, FieldFilterRule, FilterRule } from '@staticcms/core/interface';
function entryMatchesFieldRule(entry: Entry, filterRule: FieldFilterRule): boolean { export function entryMatchesFieldRule(entry: Entry, filterRule: FieldFilterRule): boolean {
const fieldValue = entry.data?.[filterRule.field]; const fieldValue = entry.data?.[filterRule.field];
if ('pattern' in filterRule) { if ('pattern' in filterRule) {
if (Array.isArray(fieldValue)) { if (Array.isArray(fieldValue)) {

View File

@ -49,7 +49,6 @@ interface SortableItemProps {
submitted: boolean; submitted: boolean;
disabled: boolean; disabled: boolean;
duplicate: boolean; duplicate: boolean;
hidden: boolean;
locale: string | undefined; locale: string | undefined;
path: string; path: string;
value: Record<string, ObjectValue>; value: Record<string, ObjectValue>;
@ -68,7 +67,6 @@ const SortableItem: FC<SortableItemProps> = ({
submitted, submitted,
disabled, disabled,
duplicate, duplicate,
hidden,
locale, locale,
path, path,
i18n, i18n,
@ -112,7 +110,6 @@ const SortableItem: FC<SortableItemProps> = ({
submitted={submitted} submitted={submitted}
disabled={disabled} disabled={disabled}
duplicate={duplicate} duplicate={duplicate}
hidden={hidden}
locale={locale} locale={locale}
path={path} path={path}
value={item} value={item}
@ -185,7 +182,6 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
submitted, submitted,
disabled, disabled,
duplicate, duplicate,
hidden,
locale, locale,
path, path,
value, value,
@ -346,7 +342,6 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
submitted={submitted} submitted={submitted}
disabled={disabled} disabled={disabled}
duplicate={duplicate} duplicate={duplicate}
hidden={hidden}
locale={locale} locale={locale}
path={path} path={path}
value={item as Record<string, ObjectValue>} value={item as Record<string, ObjectValue>}

View File

@ -69,7 +69,6 @@ interface ListItemProps
| 'submitted' | 'submitted'
| 'disabled' | 'disabled'
| 'duplicate' | 'duplicate'
| 'hidden'
| 'locale' | 'locale'
| 'path' | 'path'
| 'value' | 'value'
@ -91,7 +90,6 @@ const ListItem: FC<ListItemProps> = ({
submitted, submitted,
disabled, disabled,
duplicate, duplicate,
hidden,
locale, locale,
path, path,
valueType, valueType,
@ -206,7 +204,6 @@ const ListItem: FC<ListItemProps> = ({
parentPath={path} parentPath={path}
disabled={disabled || duplicate} disabled={disabled || duplicate}
parentDuplicate={duplicate} parentDuplicate={duplicate}
parentHidden={hidden}
locale={locale} locale={locale}
i18n={i18n} i18n={i18n}
forList={true} forList={true}

View File

@ -16,7 +16,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
forList, forList,
forSingleList, forSingleList,
duplicate, duplicate,
hidden,
locale, locale,
path, path,
i18n, i18n,
@ -57,7 +56,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
parentPath={parentPath} parentPath={parentPath}
disabled={disabled || duplicate} disabled={disabled || duplicate}
parentDuplicate={duplicate} parentDuplicate={duplicate}
parentHidden={hidden}
locale={locale} locale={locale}
i18n={i18n} i18n={i18n}
forSingleList={forSingleList} forSingleList={forSingleList}
@ -74,7 +72,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
submitted, submitted,
disabled, disabled,
duplicate, duplicate,
hidden,
locale, locale,
i18n, i18n,
forSingleList, forSingleList,

View File

@ -71,7 +71,6 @@ export const createMockWidgetControlProps = <
locale: undefined, locale: undefined,
i18n: undefined, i18n: undefined,
duplicate: false, duplicate: false,
hidden: false,
controlled: false, controlled: false,
theme: 'light', theme: 'light',
onChange: jest.fn(), onChange: jest.fn(),

View File

@ -87,14 +87,35 @@ collections:
- label: Question - label: Question
name: title name: title
widget: string widget: string
- label: Type
name: type
widget: select
default: internal
options:
- label: Internal
value: internal
- label: External
value: external
- label: URL
name: url
widget: string
condition:
field: type
value: external
- label: Answer - label: Answer
name: body name: body
widget: markdown widget: markdown
condition:
field: type
value: internal
- name: posts - name: posts
label: Posts label: Posts
label_singular: Post label_singular: Post
widget: list widget: list
summary: "{{fields.post | split('|', '$1')}}" summary: "{{fields.post | split('|', '$1')}}"
condition:
field: type
value: internal
fields: fields:
- label: Related Post - label: Related Post
name: post name: post

View File

@ -15,7 +15,7 @@ weight: 9
| icon | string | | _Optional_. Unique name of icon to use in main menu. See [custom icons](/docs/custom-icons) | | icon | string | | _Optional_. Unique name of icon to use in main menu. See [custom icons](/docs/custom-icons) |
| description | string | | _Optional_. Text displayed below the label when viewing a collection | | description | string | | _Optional_. Text displayed below the label when viewing a collection |
| files or folder | [Collection Files](/docs/collection-types#file-collections)<br />\| [Collection Folder](/docs/collection-types#folder-collections) | | **Requires one of these**: Specifies the collection type and location; details in [collection types](/docs/collection-types) | | files or folder | [Collection Files](/docs/collection-types#file-collections)<br />\| [Collection Folder](/docs/collection-types#folder-collections) | | **Requires one of these**: Specifies the collection type and location; details in [collection types](/docs/collection-types) |
| filter | FilterRule<br />\| List of FilterRules | | _Optional_. Field and file filter for [Folder Collections](/docs/collection-types#folder-collections). See [filtered folder collections](/docs/collection-types#filtered-folder-collections) | | filter | FilterRule<br />\| List of FilterRules | | _Optional_. Field and file filter for [Folder Collections](/docs/collection-types#folder-collections). See [filtered folder collections](/docs/collection-types#filtered-folder-collections) |
| create | boolean | `false` | _Optional_. **For [Folder Collections](/docs/collection-types#folder-collections) only**<br />`true` - Allows users to create new items in the collection | | create | boolean | `false` | _Optional_. **For [Folder Collections](/docs/collection-types#folder-collections) only**<br />`true` - Allows users to create new items in the collection |
| hide | boolean | `false` | _Optional_. `true` hides a collection in the Static CMS UI. Useful when using the relation widget to hide referenced collections | | hide | boolean | `false` | _Optional_. `true` hides a collection in the Static CMS UI. Useful when using the relation widget to hide referenced collections |
| delete | boolean | `true` | _Optional_. `false` prevents users from deleting items in a collection | | delete | boolean | `true` | _Optional_. `false` prevents users from deleting items in a collection |

View File

@ -103,7 +103,7 @@ collections: [
</CodeTabs> </CodeTabs>
### Filtered folder collections ### Filtered Folder Collections
The entries for any folder collection can be filtered based on the values of the fields or on file names. By filtering a folder into different collections, you can manage files with different fields, options, extensions, etc. in the same folder. The entries for any folder collection can be filtered based on the values of the fields or on file names. By filtering a folder into different collections, you can manage files with different fields, options, extensions, etc. in the same folder.

View File

@ -30,22 +30,35 @@ To see working examples of all of the built-in widgets, try making a 'Kitchen Si
| [Select](/docs/widget-select) | The select widget allows you to pick a string value from a dropdown menu | | [Select](/docs/widget-select) | The select widget allows you to pick a string value from a dropdown menu |
| [String](/docs/widget-string) | The string widget translates a basic text input to a string value | | [String](/docs/widget-string) | The string widget translates a basic text input to a string value |
| [Text](/docs/widget-text) | The text widget takes a multiline text field and saves it as a string | | [Text](/docs/widget-text) | The text widget takes a multiline text field and saves it as a string |
| [UUID](/docs/widget-uuid) | The uuid widget generates a unique id (uuid) and saves it as a string | | [UUID](/docs/widget-uuid) | The uuid widget generates a unique id (uuid) and saves it as a string |
## Common widget options ## Common widget options
The following options are available on all fields: The following options are available on all fields:
| Name | Type | Default | Description | | Name | Type | Default | Description |
| -------- | ----------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | --------- | -------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| name | string | | The name of the field | | name | string | | The name of the field |
| widget | string | `'string'` | _Optional_. The type of widget to render for the field | | widget | string | `'string'` | _Optional_. The type of widget to render for the field |
| label | string | `name` | _Optional_. The display name of the field | | label | string | `name` | _Optional_. The display name of the field |
| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional | | required | boolean | `true` | _Optional_. Specify as `false` to make a field optional |
| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. | | hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. |
| pattern | list of strings | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) | | pattern | list of strings | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) |
| i18n | boolean<br />\|'translate'<br />\|'duplicate'<br />\|'none' | | _Optional_. <BetaImage /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> | | i18n | boolean<br />\| 'translate'<br />\| 'duplicate'<br />\| 'none' | | _Optional_. <BetaImage /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> |
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) | | condition | FilterRule<br />\| List of FilterRules | | _Optional_. See [Field Conditions](#field-conditions) |
## Field Conditions
The fields can be shown conditionally based on the values of the other fields.
The `condition` option can take a single filter rule or a list of filter rules.
| Name | Type | Default | Description |
| -------- | ------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| field | string | | The name of one of the field. |
| value | string<br />\| list of strings | | _Optional_. The desired value or values to match. Required if no `pattern` provided. Ignored if `pattern` is provided |
| pattern | regular expression | | _Optional_. A regex pattern to match against the field's value |
| matchAll | boolean | `false` | _Optional_. _Ignored if value is not a list of strings_<br /><ul><li>`true` - The field's values must include or match all of the filter rule's values</li><li>`false` - The field's value must include or match only one of the filter rule's values</li></ul> |
## Example ## Example