diff --git a/packages/netlify-cms-widget-list/package.json b/packages/netlify-cms-widget-list/package.json index 606626ee..4fa6d77b 100644 --- a/packages/netlify-cms-widget-list/package.json +++ b/packages/netlify-cms-widget-list/package.json @@ -31,6 +31,7 @@ "lodash": "^4.17.11", "netlify-cms-ui-default": "^2.6.0", "netlify-cms-widget-object": "^2.2.0", + "netlify-cms-lib-widgets": "^1.0.0", "prop-types": "^15.7.2", "react": "^16.8.4", "react-immutable-proptypes": "^2.1.0" diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index ebcbe7f5..85d08875 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -15,6 +15,7 @@ import { getErrorMessageForTypedFieldAndValue, } from './typedListHelpers'; import { ListItemTopBar, ObjectWidgetTopBar, colors, lengths } from 'netlify-cms-ui-default'; +import { stringTemplate } from 'netlify-cms-lib-widgets'; function valueToString(value) { return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : ''; @@ -71,6 +72,14 @@ const valueTypes = { MIXED: 'MIXED', }; +const handleSummary = (summary, entry, label, item) => { + const data = stringTemplate.addFileTemplateFields( + entry.get('path'), + item.set('fields.label', label), + ); + return stringTemplate.compileStringTemplate(summary, null, '', data); +}; + export default class ListControl extends React.Component { validations = []; @@ -96,6 +105,7 @@ export default class ListControl extends React.Component { resolveWidget: PropTypes.func.isRequired, clearFieldErrors: PropTypes.func.isRequired, fieldsErrors: ImmutablePropTypes.map.isRequired, + entry: ImmutablePropTypes.map.isRequired, }; static defaultProps = { @@ -317,17 +327,35 @@ export default class ListControl extends React.Component { }; objectLabel(item) { - const { field } = this.props; - if (this.getValueType() === valueTypes.MIXED) { - return getTypedFieldForValue(field, item).get('label', field.get('name')); + const { field, entry } = this.props; + const valueType = this.getValueType(); + switch (valueType) { + case valueTypes.MIXED: { + 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 + const summary = itemType.get('summary', field.get('summary')); + const labelReturn = summary ? handleSummary(summary, entry, label, item) : label; + return labelReturn; + } + case valueTypes.SINGLE: { + const singleField = field.get('field'); + const label = singleField.get('label', singleField.get('name')); + const summary = field.get('summary'); + const data = fromJS({ [singleField.get('name')]: item }); + const labelReturn = summary ? handleSummary(summary, entry, label, data) : label; + return labelReturn; + } + case valueTypes.MULTIPLE: { + const multiFields = field.get('fields'); + const labelField = multiFields && multiFields.first(); + const value = item.get(labelField.get('name')); + const summary = field.get('summary'); + const labelReturn = summary ? handleSummary(summary, entry, value, item) : value; + return (labelReturn || `No ${labelField.get('name')}`).toString(); + } } - const multiFields = field.get('fields'); - const singleField = field.get('field'); - const labelField = (multiFields && multiFields.first()) || singleField; - const value = multiFields - ? item.get(multiFields.first().get('name')) - : singleField.get('label'); - return (value || `No ${labelField.get('name')}`).toString(); + return ''; } onSortEnd = ({ oldIndex, newIndex }) => { diff --git a/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js index 606a5c19..2a9ae0a6 100644 --- a/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js +++ b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js @@ -48,6 +48,9 @@ describe('ListControl', () => { resolveWidget: jest.fn(), clearFieldErrors: jest.fn(), fieldsErrors: fromJS({}), + entry: fromJS({ + path: 'posts/index.md', + }), }; beforeEach(() => { @@ -270,4 +273,185 @@ describe('ListControl', () => { expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); }); + + it('should use widget name when no summary or label are configured for mixed types', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + types: [ + { + name: 'type_1_object', + widget: 'object', + fields: [ + { label: 'First Name', name: 'first_name', widget: 'string' }, + { label: 'Last Name', name: 'last_name', widget: 'string' }, + ], + }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('type_1_object')).toBeInTheDocument(); + }); + + it('should use label when no summary is configured for mixed types', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + types: [ + { + label: 'Type 1 Object', + name: 'type_1_object', + widget: 'object', + fields: [ + { label: 'First Name', name: 'first_name', widget: 'string' }, + { label: 'Last Name', name: 'last_name', widget: 'string' }, + ], + }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('Type 1 Object')).toBeInTheDocument(); + }); + + it('should use summary when configured for mixed types', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + types: [ + { + label: 'Type 1 Object', + name: 'type_1_object', + summary: '{{first_name}} - {{last_name}} - {{filename}}.{{extension}}', + widget: 'object', + fields: [ + { label: 'First Name', name: 'first_name', widget: 'string' }, + { label: 'Last Name', name: 'last_name', widget: 'string' }, + ], + }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('hello - world - index.md')).toBeInTheDocument(); + }); + + it('should use widget name when no summary or label are configured for a single field', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + field: { name: 'name', widget: 'string' }, + }); + + const { getByText } = render(); + expect(getByText('name')).toBeInTheDocument(); + }); + + it('should use label when no summary is configured for a single field', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + field: { name: 'name', widget: 'string', label: 'Name' }, + }); + + const { getByText } = render(); + expect(getByText('Name')).toBeInTheDocument(); + }); + + it('should use summary when configured for a single field', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + summary: 'Name - {{fields.name}}', + field: { name: 'name', widget: 'string', label: 'Name' }, + }); + + const { getByText } = render(); + expect(getByText('Name - Name')).toBeInTheDocument(); + }); + + it('should use first field value when no summary or label are configured for multiple fields', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + fields: [ + { name: 'first_name', widget: 'string', label: 'First Name' }, + { name: 'last_name', widget: 'string', label: 'Last Name' }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('hello')).toBeInTheDocument(); + }); + + it('should show `No ` when value is missing from first field for multiple fields', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + fields: [ + { name: 'first_name', widget: 'string', label: 'First Name' }, + { name: 'last_name', widget: 'string', label: 'Last Name' }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('No first_name')).toBeInTheDocument(); + }); + + it('should use summary when configured for multiple fields', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + summary: '{{first_name}} - {{last_name}} - {{filename}}.{{extension}}', + fields: [ + { name: 'first_name', widget: 'string', label: 'First Name' }, + { name: 'last_name', widget: 'string', label: 'Last Name' }, + ], + }); + + const { getByText } = render( + , + ); + expect(getByText('hello - world - index.md')).toBeInTheDocument(); + }); }); diff --git a/website/content/docs/beta-features.md b/website/content/docs/beta-features.md index 3d9c449c..4ba88dc5 100644 --- a/website/content/docs/beta-features.md +++ b/website/content/docs/beta-features.md @@ -163,6 +163,7 @@ To use variable types in the list widget, update your field configuration as fol - `types`: a nested list of object widgets. All widgets must be of type `object`. Every object widget may define different set of fields. - `typeKey`: the name of the field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined. Default is `type`. +- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary) ### Example Configuration @@ -177,6 +178,7 @@ either a "carousel" or a "spotlight". Each type has a unique name and set of fie - label: 'Carousel' name: 'carousel' widget: object + summary: '{{fields.header}}' fields: - { label: Header, name: header, widget: string, default: 'Image Gallery' } - { label: Template, name: template, widget: string, default: 'carousel.html' } diff --git a/website/content/docs/widgets/list.md b/website/content/docs/widgets/list.md index 9e733538..8dc64832 100644 --- a/website/content/docs/widgets/list.md +++ b/website/content/docs/widgets/list.md @@ -12,6 +12,7 @@ The list widget allows you to create a repeatable item in the UI which saves as - `default`: if `fields` is specified, declare defaults on the child widgets; if not, you may specify a list of strings to populate the text field - `allow_add`: if added and labeled `false`, button to add additional widgets disappears - `collapsed`: if added and labeled `false`, the list widget's content does not collapse by default + - `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary) - `field`: a single widget field to be repeated - `fields`: a nested list of multiple widget fields to be included in each repeatable iteration - **Example** (`field`/`fields` not specified): @@ -34,6 +35,7 @@ The list widget allows you to create a repeatable item in the UI which saves as - label: "Gallery" name: "galleryImages" widget: "list" + summary: '{{fields.image}}' field: {label: Image, name: image, widget: image} ``` - **Example** (with `fields`): @@ -41,6 +43,7 @@ The list widget allows you to create a repeatable item in the UI which saves as - 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