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", "lodash": "^4.17.11",
"netlify-cms-ui-default": "^2.6.0", "netlify-cms-ui-default": "^2.6.0",
"netlify-cms-widget-object": "^2.2.0", "netlify-cms-widget-object": "^2.2.0",
"netlify-cms-lib-widgets": "^1.0.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.8.4", "react": "^16.8.4",
"react-immutable-proptypes": "^2.1.0" "react-immutable-proptypes": "^2.1.0"

View File

@ -15,6 +15,7 @@ import {
getErrorMessageForTypedFieldAndValue, getErrorMessageForTypedFieldAndValue,
} from './typedListHelpers'; } from './typedListHelpers';
import { ListItemTopBar, ObjectWidgetTopBar, colors, lengths } from 'netlify-cms-ui-default'; import { ListItemTopBar, ObjectWidgetTopBar, colors, lengths } from 'netlify-cms-ui-default';
import { stringTemplate } from 'netlify-cms-lib-widgets';
function valueToString(value) { function valueToString(value) {
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : ''; return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
@ -71,6 +72,14 @@ const valueTypes = {
MIXED: 'MIXED', 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 { export default class ListControl extends React.Component {
validations = []; validations = [];
@ -96,6 +105,7 @@ export default class ListControl extends React.Component {
resolveWidget: PropTypes.func.isRequired, resolveWidget: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired, clearFieldErrors: PropTypes.func.isRequired,
fieldsErrors: ImmutablePropTypes.map.isRequired, fieldsErrors: ImmutablePropTypes.map.isRequired,
entry: ImmutablePropTypes.map.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -317,17 +327,35 @@ export default class ListControl extends React.Component {
}; };
objectLabel(item) { objectLabel(item) {
const { field } = this.props; const { field, entry } = this.props;
if (this.getValueType() === valueTypes.MIXED) { const valueType = this.getValueType();
return getTypedFieldForValue(field, item).get('label', field.get('name')); 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'); return '';
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();
} }
onSortEnd = ({ oldIndex, newIndex }) => { onSortEnd = ({ oldIndex, newIndex }) => {

View File

@ -48,6 +48,9 @@ describe('ListControl', () => {
resolveWidget: jest.fn(), resolveWidget: jest.fn(),
clearFieldErrors: jest.fn(), clearFieldErrors: jest.fn(),
fieldsErrors: fromJS({}), fieldsErrors: fromJS({}),
entry: fromJS({
path: 'posts/index.md',
}),
}; };
beforeEach(() => { beforeEach(() => {
@ -270,4 +273,185 @@ describe('ListControl', () => {
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false');
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); 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. - `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`. - `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 ### Example Configuration
@ -177,6 +178,7 @@ either a "carousel" or a "spotlight". Each type has a unique name and set of fie
- label: 'Carousel' - label: 'Carousel'
name: 'carousel' name: 'carousel'
widget: object widget: object
summary: '{{fields.header}}'
fields: fields:
- { label: Header, name: header, widget: string, default: 'Image Gallery' } - { label: Header, name: header, widget: string, default: 'Image Gallery' }
- { label: Template, name: template, widget: string, default: 'carousel.html' } - { 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 - `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 - `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 - `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 - `field`: a single widget field to be repeated
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration - `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
- **Example** (`field`/`fields` not specified): - **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" - label: "Gallery"
name: "galleryImages" name: "galleryImages"
widget: "list" widget: "list"
summary: '{{fields.image}}'
field: {label: Image, name: image, widget: image} field: {label: Image, name: image, widget: image}
``` ```
- **Example** (with `fields`): - **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" - label: "Testimonials"
name: "testimonials" name: "testimonials"
widget: "list" widget: "list"
summary: '{{fields.quote}} - {{fields.author.name}}'
fields: fields:
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
- label: Author - label: Author