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
|
- 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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
`,
|
`,
|
||||||
|
@ -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(
|
||||||
`
|
`
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
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,
|
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,
|
||||||
]);
|
]);
|
||||||
|
@ -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]);
|
||||||
|
}
|
||||||
|
@ -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)) {
|
||||||
|
@ -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>}
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
@ -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
|
||||||
|
@ -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 |
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user