Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(netlify-cms-widget-list): allow 'summary' field #3616

Merged
merged 6 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/netlify-cms-widget-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 38 additions & 10 deletions packages/netlify-cms-widget-list/src/ListControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') : '';
Expand Down Expand Up @@ -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 = [];

Expand All @@ -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 = {
Expand Down Expand Up @@ -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 }) => {
Expand Down
184 changes: 184 additions & 0 deletions packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ describe('ListControl', () => {
resolveWidget: jest.fn(),
clearFieldErrors: jest.fn(),
fieldsErrors: fromJS({}),
entry: fromJS({
path: 'posts/index.md',
}),
};

beforeEach(() => {
Expand Down Expand Up @@ -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(
<ListControl
{...props}
field={field}
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
/>,
);
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(
<ListControl
{...props}
field={field}
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
/>,
);
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(
<ListControl
{...props}
field={field}
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
/>,
);
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(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
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(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
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(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
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(
<ListControl
{...props}
field={field}
value={fromJS([{ first_name: 'hello', last_name: 'world' }])}
/>,
);
expect(getByText('hello')).toBeInTheDocument();
});

it('should show `No <field>` 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(
<ListControl {...props} field={field} value={fromJS([{ last_name: 'world' }])} />,
);
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(
<ListControl
{...props}
field={field}
value={fromJS([{ first_name: 'hello', last_name: 'world' }])}
/>,
);
expect(getByText('hello - world - index.md')).toBeInTheDocument();
});
});
2 changes: 2 additions & 0 deletions website/content/docs/beta-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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' }
Expand Down
3 changes: 3 additions & 0 deletions website/content/docs/widgets/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -34,13 +35,15 @@ 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`):
```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
Expand Down