diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
new file mode 100644
index 00000000..9d8cf4aa
--- /dev/null
+++ b/BREAKING_CHANGES.md
@@ -0,0 +1,2 @@
+- gitea API config has changed.
+- hidden removed as widget parameter
\ No newline at end of file
diff --git a/packages/core/dev-test/config.yml b/packages/core/dev-test/config.yml
index 072dfd6e..9caef869 100644
--- a/packages/core/dev-test/config.yml
+++ b/packages/core/dev-test/config.yml
@@ -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
diff --git a/packages/core/src/components/collections/mobile/MobileCollectionControlsDrawer.tsx b/packages/core/src/components/collections/mobile/MobileCollectionControlsDrawer.tsx
index d43500cf..4fe257ec 100644
--- a/packages/core/src/components/collections/mobile/MobileCollectionControlsDrawer.tsx
+++ b/packages/core/src/components/collections/mobile/MobileCollectionControlsDrawer.tsx
@@ -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 (
-
) : null}
-
+
);
};
diff --git a/packages/core/src/components/entry-editor/editor-control-pane/EditorControl.tsx b/packages/core/src/components/entry-editor/editor-control-pane/EditorControl.tsx
index 4ad818bd..40f595b1 100644
--- a/packages/core/src/components/entry-editor/editor-control-pane/EditorControl.tsx
+++ b/packages/core/src/components/entry-editor/editor-control-pane/EditorControl.tsx
@@ -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 (
-
+
{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;
diff --git a/packages/core/src/components/navbar/Breadcrumbs.tsx b/packages/core/src/components/navbar/Breadcrumbs.tsx
index b6b05818..71f79b87 100644
--- a/packages/core/src/components/navbar/Breadcrumbs.tsx
+++ b/packages/core/src/components/navbar/Breadcrumbs.tsx
@@ -56,6 +56,7 @@ const Breadcrumbs: FC
= ({ breadcrumbs, inEditor = false }) =>
flex
items-center
gap-1
+ pl-1
"
>
{breadcrumbs.map((breadcrumb, index) =>
@@ -73,6 +74,7 @@ const Breadcrumbs: FC = ({ breadcrumbs, inEditor = false }) =>
whitespace-nowrap
focus:outline-none
focus:ring-4
+ rounded-md
focus:ring-gray-200
dark:focus:ring-slate-700
`,
diff --git a/packages/core/src/components/navbar/Navbar.tsx b/packages/core/src/components/navbar/Navbar.tsx
index 2885b8d2..df6d75d3 100644
--- a/packages/core/src/components/navbar/Navbar.tsx
+++ b/packages/core/src/components/navbar/Navbar.tsx
@@ -71,7 +71,7 @@ const Navbar = ({
inEditor && 'pl-3 md:pl-0',
)}
>
-
+
{
- const iOS = useMemo(
- () => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
- [],
- );
-
const container = useMemo(
() => (typeof window !== 'undefined' ? window.document.body : undefined),
[],
);
return (
-
-
+
);
};
diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts
index 10112a18..f6130d9b 100644
--- a/packages/core/src/interface.ts
+++ b/packages/core/src/interface.ts
@@ -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
{
+ 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();
+ });
+ });
+});
diff --git a/packages/core/src/lib/util/__tests__/filter.util.spec.ts b/packages/core/src/lib/util/__tests__/filter.util.spec.ts
index 516ff0e1..88329caa 100644
--- a/packages/core/src/lib/util/__tests__/filter.util.spec.ts
+++ b/packages/core/src/lib/util/__tests__/filter.util.spec.ts
@@ -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,
]);
diff --git a/packages/core/src/lib/util/field.util.ts b/packages/core/src/lib/util/field.util.ts
index d4c947c4..5cfcb8e4 100644
--- a/packages/core/src/lib/util/field.util.ts
+++ b/packages/core/src/lib/util/field.util.ts
@@ -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]);
+}
diff --git a/packages/core/src/lib/util/filter.util.ts b/packages/core/src/lib/util/filter.util.ts
index 8b981346..dd5a60ed 100644
--- a/packages/core/src/lib/util/filter.util.ts
+++ b/packages/core/src/lib/util/filter.util.ts
@@ -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)) {
diff --git a/packages/core/src/widgets/list/ListControl.tsx b/packages/core/src/widgets/list/ListControl.tsx
index 32c9db05..49204e25 100644
--- a/packages/core/src/widgets/list/ListControl.tsx
+++ b/packages/core/src/widgets/list/ListControl.tsx
@@ -49,7 +49,6 @@ interface SortableItemProps {
submitted: boolean;
disabled: boolean;
duplicate: boolean;
- hidden: boolean;
locale: string | undefined;
path: string;
value: Record;
@@ -68,7 +67,6 @@ const SortableItem: FC = ({
submitted,
disabled,
duplicate,
- hidden,
locale,
path,
i18n,
@@ -112,7 +110,6 @@ const SortableItem: FC = ({
submitted={submitted}
disabled={disabled}
duplicate={duplicate}
- hidden={hidden}
locale={locale}
path={path}
value={item}
@@ -185,7 +182,6 @@ const ListControl: FC> = pro
submitted,
disabled,
duplicate,
- hidden,
locale,
path,
value,
@@ -346,7 +342,6 @@ const ListControl: FC> = pro
submitted={submitted}
disabled={disabled}
duplicate={duplicate}
- hidden={hidden}
locale={locale}
path={path}
value={item as Record}
diff --git a/packages/core/src/widgets/list/components/ListItem.tsx b/packages/core/src/widgets/list/components/ListItem.tsx
index 2c6d8174..3ab789e7 100644
--- a/packages/core/src/widgets/list/components/ListItem.tsx
+++ b/packages/core/src/widgets/list/components/ListItem.tsx
@@ -69,7 +69,6 @@ interface ListItemProps
| 'submitted'
| 'disabled'
| 'duplicate'
- | 'hidden'
| 'locale'
| 'path'
| 'value'
@@ -91,7 +90,6 @@ const ListItem: FC = ({
submitted,
disabled,
duplicate,
- hidden,
locale,
path,
valueType,
@@ -206,7 +204,6 @@ const ListItem: FC = ({
parentPath={path}
disabled={disabled || duplicate}
parentDuplicate={duplicate}
- parentHidden={hidden}
locale={locale}
i18n={i18n}
forList={true}
diff --git a/packages/core/src/widgets/object/ObjectControl.tsx b/packages/core/src/widgets/object/ObjectControl.tsx
index 0e0ec50b..741e5b1f 100644
--- a/packages/core/src/widgets/object/ObjectControl.tsx
+++ b/packages/core/src/widgets/object/ObjectControl.tsx
@@ -16,7 +16,6 @@ const ObjectControl: FC> = ({
forList,
forSingleList,
duplicate,
- hidden,
locale,
path,
i18n,
@@ -57,7 +56,6 @@ const ObjectControl: FC> = ({
parentPath={parentPath}
disabled={disabled || duplicate}
parentDuplicate={duplicate}
- parentHidden={hidden}
locale={locale}
i18n={i18n}
forSingleList={forSingleList}
@@ -74,7 +72,6 @@ const ObjectControl: FC> = ({
submitted,
disabled,
duplicate,
- hidden,
locale,
i18n,
forSingleList,
diff --git a/packages/core/test/data/widgets.mock.ts b/packages/core/test/data/widgets.mock.ts
index ce73f6b6..5aea2090 100644
--- a/packages/core/test/data/widgets.mock.ts
+++ b/packages/core/test/data/widgets.mock.ts
@@ -71,7 +71,6 @@ export const createMockWidgetControlProps = <
locale: undefined,
i18n: undefined,
duplicate: false,
- hidden: false,
controlled: false,
theme: 'light',
onChange: jest.fn(),
diff --git a/packages/demo/public/config.yml b/packages/demo/public/config.yml
index 95b9f182..fd462016 100644
--- a/packages/demo/public/config.yml
+++ b/packages/demo/public/config.yml
@@ -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
diff --git a/packages/docs/content/docs/collection-overview.mdx b/packages/docs/content/docs/collection-overview.mdx
index 406c430c..7c09e0d9 100644
--- a/packages/docs/content/docs/collection-overview.mdx
+++ b/packages/docs/content/docs/collection-overview.mdx
@@ -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)
\| [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
\| 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
\| 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**
`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 |
diff --git a/packages/docs/content/docs/collection-types.mdx b/packages/docs/content/docs/collection-types.mdx
index 5ff57d8d..a3649328 100644
--- a/packages/docs/content/docs/collection-types.mdx
+++ b/packages/docs/content/docs/collection-types.mdx
@@ -103,7 +103,7 @@ collections: [
-### 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.
diff --git a/packages/docs/content/docs/widgets.mdx b/packages/docs/content/docs/widgets.mdx
index 4736644b..8eb259e6 100644
--- a/packages/docs/content/docs/widgets.mdx
+++ b/packages/docs/content/docs/widgets.mdx
@@ -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
\|'translate'
\|'duplicate'
\|'none' | | _Optional_. - `translate` - Allows translation of the field
- `duplicate` - Duplicates the value from the default locale
- `true` - Accept parent values as default
- `none` or `false` - Exclude field from translations
|
-| 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
\| 'translate'
\| 'duplicate'
\| 'none' | | _Optional_. - `translate` - Allows translation of the field
- `duplicate` - Duplicates the value from the default locale
- `true` - Accept parent values as default
- `none` or `false` - Exclude field from translations
|
+| condition | FilterRule
\| 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
\| 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_
- `true` - The field's values must include or match all of the filter rule's values
- `false` - The field's value must include or match only one of the filter rule's values
|
## Example