feat: dependent fields (#839)
This commit is contained in:
parent
0dc7576bb4
commit
654263dd6f
2
BREAKING_CHANGES.md
Normal file
2
BREAKING_CHANGES.md
Normal file
@ -0,0 +1,2 @@
|
||||
- gitea API config has changed.
|
||||
- hidden removed as widget parameter
|
@ -87,14 +87,35 @@ collections:
|
||||
- label: Question
|
||||
name: title
|
||||
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
|
||||
name: body
|
||||
widget: markdown
|
||||
condition:
|
||||
field: type
|
||||
value: internal
|
||||
- name: posts
|
||||
label: Posts
|
||||
label_singular: Post
|
||||
widget: list
|
||||
summary: "{{fields.post | split('|', '$1')}}"
|
||||
condition:
|
||||
field: type
|
||||
value: internal
|
||||
fields:
|
||||
- label: Related Post
|
||||
name: post
|
||||
@ -870,7 +891,7 @@ collections:
|
||||
- value: 2
|
||||
label: Another fancy label
|
||||
- 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
|
||||
name: value_and_label_with_default
|
||||
widget: select
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import FilterControl from '../FilterControl';
|
||||
@ -38,24 +38,16 @@ const MobileCollectionControlsDrawer = ({
|
||||
fields,
|
||||
onSortClick,
|
||||
}: MobileCollectionControlsDrawerProps) => {
|
||||
const iOS = useMemo(
|
||||
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
[],
|
||||
);
|
||||
|
||||
const container = useMemo(
|
||||
() => (typeof window !== 'undefined' ? window.document.body : undefined),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<SwipeableDrawer
|
||||
disableBackdropTransition={!iOS}
|
||||
disableDiscovery={iOS}
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onOpen={onMobileOpenToggle}
|
||||
onClose={onMobileOpenToggle}
|
||||
anchor="right"
|
||||
ModalProps={{
|
||||
@ -110,7 +102,7 @@ const MobileCollectionControlsDrawer = ({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SwipeableDrawer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -17,8 +17,9 @@ 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 classNames from '@staticcms/core/lib/util/classNames.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 { validate } from '@staticcms/core/lib/util/validation.util';
|
||||
import { selectFieldErrors } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
@ -49,7 +50,6 @@ const EditorControl = ({
|
||||
submitted,
|
||||
disabled = false,
|
||||
parentDuplicate = false,
|
||||
parentHidden = false,
|
||||
locale,
|
||||
mediaPaths,
|
||||
openMediaLibrary,
|
||||
@ -95,13 +95,19 @@ const EditorControl = ({
|
||||
() => parentDuplicate || isFieldDuplicate(field, locale, i18n?.defaultLocale),
|
||||
[field, i18n?.defaultLocale, parentDuplicate, locale],
|
||||
);
|
||||
const hidden = useMemo(
|
||||
() => parentHidden || isFieldHidden(field, locale, i18n?.defaultLocale),
|
||||
[field, i18n?.defaultLocale, parentHidden, locale],
|
||||
const i18nDisabled = useMemo(
|
||||
() => isFieldHidden(field, locale, i18n?.defaultLocale),
|
||||
[field, i18n?.defaultLocale, locale],
|
||||
);
|
||||
const hidden = useHidden(field, entry);
|
||||
|
||||
useEffect(() => {
|
||||
if ((!dirty && !submitted) || hidden || disabled) {
|
||||
if (hidden) {
|
||||
dispatch(changeDraftFieldValidation(path, [], i18n, isMeta));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!dirty && !submitted) || disabled || i18nDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -111,7 +117,21 @@ const EditorControl = ({
|
||||
};
|
||||
|
||||
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(
|
||||
(value: ValueOrNestedValue) => {
|
||||
@ -157,7 +177,7 @@ const EditorControl = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classNames(hidden && 'hidden')}>
|
||||
{createElement(widget.control, {
|
||||
key: `${id}-${version}`,
|
||||
collection,
|
||||
@ -167,9 +187,8 @@ const EditorControl = ({
|
||||
field: field as UnknownField,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
disabled: disabled || duplicate || hidden,
|
||||
disabled: disabled || duplicate || hidden || i18nDisabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
label: getFieldLabel(field, t),
|
||||
locale,
|
||||
mediaPaths,
|
||||
@ -226,7 +245,6 @@ interface EditorControlOwnProps {
|
||||
submitted: boolean;
|
||||
disabled?: boolean;
|
||||
parentDuplicate?: boolean;
|
||||
parentHidden?: boolean;
|
||||
locale?: string;
|
||||
parentPath: string;
|
||||
value: ValueOrNestedValue;
|
||||
|
@ -56,6 +56,7 @@ const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) =>
|
||||
flex
|
||||
items-center
|
||||
gap-1
|
||||
pl-1
|
||||
"
|
||||
>
|
||||
{breadcrumbs.map((breadcrumb, index) =>
|
||||
@ -73,6 +74,7 @@ const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) =>
|
||||
whitespace-nowrap
|
||||
focus:outline-none
|
||||
focus:ring-4
|
||||
rounded-md
|
||||
focus:ring-gray-200
|
||||
dark:focus:ring-slate-700
|
||||
`,
|
||||
|
@ -71,7 +71,7 @@ const Navbar = ({
|
||||
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
|
||||
className={classNames(
|
||||
`
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import SidebarContent from './SidebarContent';
|
||||
@ -11,24 +11,16 @@ interface NavigationDrawerProps {
|
||||
}
|
||||
|
||||
const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => {
|
||||
const iOS = useMemo(
|
||||
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
[],
|
||||
);
|
||||
|
||||
const container = useMemo(
|
||||
() => (typeof window !== 'undefined' ? window.document.body : undefined),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<SwipeableDrawer
|
||||
disableBackdropTransition={!iOS}
|
||||
disableDiscovery={iOS}
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onOpen={onMobileOpenToggle}
|
||||
onClose={onMobileOpenToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
@ -49,7 +41,7 @@ const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerPr
|
||||
<div onClick={onMobileOpenToggle} className="w-full h-full">
|
||||
<SidebarContent />
|
||||
</div>
|
||||
</SwipeableDrawer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -179,7 +179,7 @@ export interface FieldPatternFilterRule extends BaseFieldFilterRule {
|
||||
}
|
||||
|
||||
export interface FieldValueFilterRule extends BaseFieldFilterRule {
|
||||
value: string | string[];
|
||||
value: string | number | boolean | (string | number | boolean)[];
|
||||
matchAll?: boolean;
|
||||
}
|
||||
|
||||
@ -305,7 +305,6 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField, EV =
|
||||
forSingleList: boolean;
|
||||
disabled: boolean;
|
||||
duplicate: boolean;
|
||||
hidden: boolean;
|
||||
label: string;
|
||||
locale: string | undefined;
|
||||
// @deprecated Use useMediaInsert instead
|
||||
@ -603,6 +602,7 @@ export interface BaseField {
|
||||
i18n?: boolean | 'translate' | 'duplicate' | 'none';
|
||||
comment?: string;
|
||||
widget: string;
|
||||
condition?: FieldFilterRule | FieldFilterRule[];
|
||||
}
|
||||
|
||||
export interface MediaField extends BaseField {
|
||||
|
100
packages/core/src/lib/util/__tests__/field.util.spec.ts
Normal file
100
packages/core/src/lib/util/__tests__/field.util.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -88,7 +88,7 @@ describe('filterEntries', () => {
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
|
||||
expect(filterEntries(entries, { field: 'draft', value: 'true' })).toEqual([
|
||||
expect(filterEntries(entries, { field: 'draft', value: true })).toEqual([
|
||||
mockEnglishEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
@ -119,7 +119,7 @@ describe('filterEntries', () => {
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
|
||||
expect(filterEntries(entries, { field: 'numbers', value: '8' })).toEqual([
|
||||
expect(filterEntries(entries, { field: 'numbers', value: 8 })).toEqual([
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import get from 'lodash/get';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getLocaleDataPath } from '../i18n';
|
||||
import { keyToPathArray } from '../widgets/stringTemplate';
|
||||
import { entryMatchesFieldRule } from './filter.util';
|
||||
|
||||
import type {
|
||||
BaseField,
|
||||
@ -88,3 +90,23 @@ export function getFieldValue(
|
||||
|
||||
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]);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { parse } from 'path';
|
||||
|
||||
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];
|
||||
if ('pattern' in filterRule) {
|
||||
if (Array.isArray(fieldValue)) {
|
||||
|
@ -49,7 +49,6 @@ interface SortableItemProps {
|
||||
submitted: boolean;
|
||||
disabled: boolean;
|
||||
duplicate: boolean;
|
||||
hidden: boolean;
|
||||
locale: string | undefined;
|
||||
path: string;
|
||||
value: Record<string, ObjectValue>;
|
||||
@ -68,7 +67,6 @@ const SortableItem: FC<SortableItemProps> = ({
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
i18n,
|
||||
@ -112,7 +110,6 @@ const SortableItem: FC<SortableItemProps> = ({
|
||||
submitted={submitted}
|
||||
disabled={disabled}
|
||||
duplicate={duplicate}
|
||||
hidden={hidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item}
|
||||
@ -185,7 +182,6 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
value,
|
||||
@ -346,7 +342,6 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
|
||||
submitted={submitted}
|
||||
disabled={disabled}
|
||||
duplicate={duplicate}
|
||||
hidden={hidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
|
@ -69,7 +69,6 @@ interface ListItemProps
|
||||
| 'submitted'
|
||||
| 'disabled'
|
||||
| 'duplicate'
|
||||
| 'hidden'
|
||||
| 'locale'
|
||||
| 'path'
|
||||
| 'value'
|
||||
@ -91,7 +90,6 @@ const ListItem: FC<ListItemProps> = ({
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
valueType,
|
||||
@ -206,7 +204,6 @@ const ListItem: FC<ListItemProps> = ({
|
||||
parentPath={path}
|
||||
disabled={disabled || duplicate}
|
||||
parentDuplicate={duplicate}
|
||||
parentHidden={hidden}
|
||||
locale={locale}
|
||||
i18n={i18n}
|
||||
forList={true}
|
||||
|
@ -16,7 +16,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
|
||||
forList,
|
||||
forSingleList,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
i18n,
|
||||
@ -57,7 +56,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
|
||||
parentPath={parentPath}
|
||||
disabled={disabled || duplicate}
|
||||
parentDuplicate={duplicate}
|
||||
parentHidden={hidden}
|
||||
locale={locale}
|
||||
i18n={i18n}
|
||||
forSingleList={forSingleList}
|
||||
@ -74,7 +72,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
i18n,
|
||||
forSingleList,
|
||||
|
@ -71,7 +71,6 @@ export const createMockWidgetControlProps = <
|
||||
locale: undefined,
|
||||
i18n: undefined,
|
||||
duplicate: false,
|
||||
hidden: false,
|
||||
controlled: false,
|
||||
theme: 'light',
|
||||
onChange: jest.fn(),
|
||||
|
@ -87,14 +87,35 @@ collections:
|
||||
- label: Question
|
||||
name: title
|
||||
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
|
||||
name: body
|
||||
widget: markdown
|
||||
condition:
|
||||
field: type
|
||||
value: internal
|
||||
- name: posts
|
||||
label: Posts
|
||||
label_singular: Post
|
||||
widget: list
|
||||
summary: "{{fields.post | split('|', '$1')}}"
|
||||
condition:
|
||||
field: type
|
||||
value: internal
|
||||
fields:
|
||||
- label: Related Post
|
||||
name: post
|
||||
|
@ -15,7 +15,7 @@ weight: 9
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
@ -103,7 +103,7 @@ collections: [
|
||||
|
||||
</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.
|
||||
|
||||
|
@ -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 |
|
||||
| [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 |
|
||||
| [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
|
||||
|
||||
The following options are available on all fields:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ----------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| name | string | | The name of 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 |
|
||||
| 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. |
|
||||
| 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> |
|
||||
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) |
|
||||
| Name | Type | Default | Description |
|
||||
| --------- | -------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| name | string | | The name of 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 |
|
||||
| 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. |
|
||||
| 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> |
|
||||
| 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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user