feat(netlify-cms-widget-list): allow 'summary' field (#3616)
This commit is contained in:
parent
c9a2fec2da
commit
7cc4c89539
@ -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"
|
||||
|
@ -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 }) => {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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' }
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user