From 83c235423e76023fe10f167c8f9087cba7a4c922 Mon Sep 17 00:00:00 2001 From: Pearce <32177944+Pearce-Ropion@users.noreply.github.com> Date: Mon, 5 Apr 2021 08:54:07 -0700 Subject: [PATCH] fix: allow any default list as default value for list widgets (#5030) --- .../src/actions/__tests__/entries.spec.js | 121 +++++++------ .../netlify-cms-core/src/actions/entries.ts | 40 ++--- .../EditorPreviewPane/EditorPreviewPane.js | 2 +- .../src/ListControl.js | 17 ++ website/content/docs/widgets/list.md | 160 ++++++++++-------- 5 files changed, 193 insertions(+), 147 deletions(-) diff --git a/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js b/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js index 33514634..c74ab054 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js @@ -146,6 +146,28 @@ describe('entries', () => { expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); }); + it('should allow a complex array as list default for a single field list', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + default: [ + { + url: 'https://image.png', + }, + ], + field: { name: 'url', widget: 'text' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + images: fromJS([ + { + url: 'https://image.png', + }, + ]), + }); + }); + it('should allow an empty array as list default for a fields list', () => { const fields = fromJS([ { @@ -161,78 +183,49 @@ describe('entries', () => { expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); }); - it('should not allow setting a non empty array as a default value for a single field list', () => { + it('should allow a complex array as list default for a fields list', () => { const fields = fromJS([ { name: 'images', widget: 'list', - default: [{ name: 'url' }, { other: 'field' }], - field: { name: 'url', widget: 'text' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({}); - }); - - it('should not allow setting a non empty array as a default value for a fields list', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [{ name: 'url' }, { other: 'field' }], + default: [ + { + title: 'default image', + url: 'https://image.png', + }, + ], fields: [ { name: 'title', widget: 'text' }, { name: 'url', widget: 'text' }, ], }, ]); - expect(createEmptyDraftData(fields)).toEqual({}); - }); - - it('should set default value for list field widget', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - field: { name: 'url', widget: 'text', default: 'https://image.png' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ images: ['https://image.png'] }); - }); - - it('should override list default with field default', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [], - field: { name: 'url', widget: 'text', default: 'https://image.png' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ images: ['https://image.png'] }); - }); - - it('should set default values for list fields widget', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - fields: [ - { name: 'title', widget: 'text', default: 'default image' }, - { name: 'url', widget: 'text', default: 'https://image.png' }, - ], - }, - ]); expect(createEmptyDraftData(fields)).toEqual({ - images: [{ title: 'default image', url: 'https://image.png' }], + images: fromJS([ + { + title: 'default image', + url: 'https://image.png', + }, + ]), }); }); - it('should override list default with fields default', () => { + it('should use field default when no list default is provided', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + field: { name: 'url', widget: 'text', default: 'https://image.png' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ images: [{ url: 'https://image.png' }] }); + }); + + it('should use fields default when no list default is provided', () => { const fields = fromJS([ { name: 'images', widget: 'list', - default: [], fields: [ { name: 'title', widget: 'text', default: 'default image' }, { name: 'url', widget: 'text', default: 'https://image.png' }, @@ -298,6 +291,26 @@ describe('entries', () => { ]); expect(createEmptyDraftData(fields)).toEqual({}); }); + + it('should populate nested fields', () => { + const fields = fromJS([ + { + name: 'names', + widget: 'list', + field: { + name: 'object', + widget: 'object', + fields: [ + { name: 'first', widget: 'string', default: 'first' }, + { name: 'second', widget: 'string', default: 'second' }, + ], + }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + names: [{ object: { first: 'first', second: 'second' } }], + }); + }); }); describe('persistLocalBackup', () => { diff --git a/packages/netlify-cms-core/src/actions/entries.ts b/packages/netlify-cms-core/src/actions/entries.ts index 9869c153..04666180 100644 --- a/packages/netlify-cms-core/src/actions/entries.ts +++ b/packages/netlify-cms-core/src/actions/entries.ts @@ -771,7 +771,6 @@ interface DraftEntryData { export function createEmptyDraftData( fields: EntryFields, - withNameKey = true, skipField: (field: EntryField) => boolean = () => false, ) { return fields.reduce( @@ -795,36 +794,27 @@ export function createEmptyDraftData( return [[{}], {}].some(e => isEqual(val, e)); } - if (List.isList(subfields)) { - const subDefaultValue = list - ? [createEmptyDraftData(subfields as EntryFields, withNameKey, skipField)] - : createEmptyDraftData(subfields as EntryFields, withNameKey, skipField); - if (!isEmptyDefaultValue(subDefaultValue)) { - acc[name] = subDefaultValue; - } else if (list && List.isList(defaultValue) && (defaultValue as List).isEmpty()) { - // allow setting an empty list as a default + const hasSubfields = List.isList(subfields) || Map.isMap(subfields); + if (hasSubfields) { + if (list && List.isList(defaultValue)) { acc[name] = defaultValue; - } - return acc; - } + } else { + const asList = List.isList(subfields) + ? (subfields as EntryFields) + : List([subfields as EntryField]); - if (Map.isMap(subfields)) { - const subDefaultValue = list - ? [createEmptyDraftData(List([subfields as EntryField]), false, skipField)] - : createEmptyDraftData(List([subfields as EntryField]), withNameKey, skipField); - if (!isEmptyDefaultValue(subDefaultValue)) { - acc[name] = subDefaultValue; - } else if (list && List.isList(defaultValue) && (defaultValue as List).isEmpty()) { - // allow setting an empty list as a default - acc[name] = defaultValue; + const subDefaultValue = list + ? [createEmptyDraftData(asList, skipField)] + : createEmptyDraftData(asList, skipField); + + if (!isEmptyDefaultValue(subDefaultValue)) { + acc[name] = subDefaultValue; + } } return acc; } if (defaultValue !== null) { - if (!withNameKey) { - return defaultValue; - } acc[name] = defaultValue; } @@ -843,7 +833,7 @@ function createEmptyDraftI18nData(collection: Collection, dataFields: EntryField return field.get(I18N) !== I18N_FIELD.DUPLICATE && field.get(I18N) !== I18N_FIELD.TRANSLATE; } - const i18nData = createEmptyDraftData(dataFields, true, skipField); + const i18nData = createEmptyDraftData(dataFields, skipField); return duplicateDefaultI18nFields(collection, i18nData); } diff --git a/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js b/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js index 1692a962..28d945a3 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js @@ -76,7 +76,7 @@ export class PreviewPane extends React.Component { // We retrieve the field by name so that this function can also be used in // custom preview templates, where the field object can't be passed in. let field = fields && fields.find(f => f.get('name') === name); - let value = values && values.get(field.get('name')); + let value = Map.isMap(values) && values.get(field.get('name')); if (field.get('meta')) { value = this.props.entry.getIn(['meta', field.get('name')]); } diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index 1972ef1b..e1bf47c6 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -80,6 +80,17 @@ function handleSummary(summary, entry, label, item) { return stringTemplate.compileStringTemplate(summary, null, '', data); } +function validateItem(field, item) { + if (!Map.isMap(item)) { + console.warn( + `'${field.get('name')}' field item value value should be a map but is a '${typeof item}'`, + ); + return false; + } + + return true; +} + export default class ListControl extends React.Component { validations = []; @@ -386,6 +397,9 @@ export default class ListControl extends React.Component { const valueType = this.getValueType(); switch (valueType) { case valueTypes.MIXED: { + if (!validateItem(field, item)) { + return; + } const itemType = getTypedFieldForValue(field, item); const label = itemType.get('label', itemType.get('name')); // each type can have its own summary, but default to the list summary if exists @@ -402,6 +416,9 @@ export default class ListControl extends React.Component { return labelReturn; } case valueTypes.MULTIPLE: { + if (!validateItem(field, item)) { + return; + } const multiFields = field.get('fields'); const labelField = multiFields && multiFields.first(); const value = item.get(labelField.get('name')); diff --git a/website/content/docs/widgets/list.md b/website/content/docs/widgets/list.md index 79eeacbd..7d0adf7a 100644 --- a/website/content/docs/widgets/list.md +++ b/website/content/docs/widgets/list.md @@ -9,7 +9,10 @@ The list widget allows you to create a repeatable item in the UI which saves as * **Data type:** list of widget values * **Options:** - * `default`: you may specify a list of strings to populate the basic text field, but declare defaults on the child widgets if you are specifying `fields`; + * `default`: you may specify a list of strings to populate the basic text + field, or an array of list items for lists using the `fields` option. If no + default is declared when using `field` or `fields`, will default to a single + list item using the defaults on the child widgets * `allow_add`: `false` hides the button to add additional items * `collapsed`: when `true`, the entries collapse by default * `summary`: specify the label displayed on collapsed entries @@ -20,84 +23,107 @@ The list widget allows you to create a repeatable item in the UI which saves as * `max`: maximum number of items in the list * `min`: minimum number of items in the list * `add_to_top`: when `true`, new entries will be added to the top of the list + * **Example** (`field`/`fields` not specified): - ```yaml - - label: "Tags" - name: "tags" - widget: "list" - default: ["news"] - ``` +```yaml +- label: "Tags" + name: "tags" + widget: "list" + default: ["news"] +``` + * **Example** (`allow_add` marked `false`): - ```yaml - - label: "Tags" - name: "tags" - widget: "list" - allow_add: false - default: ["news"] - ``` +```yaml +- label: "Tags" + name: "tags" + widget: "list" + allow_add: false + default: ["news"] +``` + * **Example** (with `field`): - ```yaml - - label: "Gallery" - name: "galleryImages" - widget: "list" - summary: '{{fields.image}}' - field: {label: Image, name: image, widget: image} - ``` +```yaml +- label: "Gallery" + name: "galleryImages" + widget: "list" + summary: '{{fields.image}}' + field: {label: Image, name: image, widget: image} +``` + * **Example** (with `fields`): - ```yaml - - label: "Testimonials" - name: "testimonials" - widget: "list" - summary: '{{fields.quote}} - {{fields.author.name}}' - fields: - - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} - - label: Author - name: author - widget: object - fields: - - {label: Name, name: name, widget: string, default: "Emmet"} - - {label: Avatar, name: avatar, widget: image, default: "/img/emmet.jpg"} - ``` +```yaml +- label: "Testimonials" + name: "testimonials" + widget: "list" + summary: '{{fields.quote}} - {{fields.author.name}}' + fields: + - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} + - label: Author + name: author + widget: object + fields: + - {label: Name, name: name, widget: string, default: "Emmet"} + - {label: Avatar, name: avatar, widget: image, default: "/img/emmet.jpg"} +``` + +* **Example** (with `default`): + +```yaml +- label: "Gallery" + name: "galleryImages" + widget: "list" + fields: + - { label: "Source", name: "src", widget: "string" } + - { label: "Alt Text", name: "alt", widget: "string" } + default: + - { src: "/img/tenis.jpg", alt: "Tenis" } + - { src: "/img/footbar.jpg", alt: "Football" } +``` + * **Example** (`collapsed` marked `false`): - ```yaml - - label: "Testimonials" - name: "testimonials" - collapsed: false - widget: "list" - fields: - - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} - - {label: Author, name: author, widget: string } - ``` +```yaml +- label: "Testimonials" + name: "testimonials" + collapsed: false + widget: "list" + fields: + - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} + - {label: Author, name: author, widget: string } +``` + * **Example** (`minimize_collapsed` marked `true`): - ```yaml - - label: "Testimonials" - name: "testimonials" - minimize_collapsed: true - widget: "list" - fields: - - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} - - {label: Author, name: author, widget: string } - ``` +```yaml +- label: "Testimonials" + name: "testimonials" + minimize_collapsed: true + widget: "list" + fields: + - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} + - {label: Author, name: author, widget: string } +``` * **Example** (with `max` & `min`): - ```yaml - - label: "Tags" - name: "tags" - widget: "list" - max: 3 - min: 1 - default: ["news"] - ``` + +```yaml +- label: "Tags" + name: "tags" + widget: "list" + max: 3 + min: 1 + default: ["news"] +``` + * **Example** (`add_to_top` marked `true`): - ```yaml - - label: "Tags" - name: "tags" - widget: "list" - add_to_top: true - ``` \ No newline at end of file + +```yaml +- label: "Tags" + name: "tags" + widget: "list" + add_to_top: true +```