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