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

This commit is contained in:
Derek Nguyen 2020-05-11 22:30:29 +09:00 committed by GitHub
parent c9a2fec2da
commit 7cc4c89539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 10 deletions

View File

@ -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"

View File

@ -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 }) => {

View File

@ -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(
<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();
});
});

View File

@ -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' }

View File

@ -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