feat(widget-relation): support nested field references in relation widget (#2391)

This commit is contained in:
Henry 2019-06-24 17:32:02 -06:00 committed by Shawn Erquhart
parent 50dc371ebc
commit d6964b50b3
4 changed files with 108 additions and 15 deletions

View File

@ -130,7 +130,12 @@ const commitMessageFormatter = (type, config, { slug, path, collection }) => {
const extractSearchFields = searchFields => entry =>
searchFields.reduce((acc, field) => {
const f = entry.data[field];
let nestedFields = field.split('.');
let f = entry.data;
for (let i = 0; i < nestedFields.length; i++) {
f = f[nestedFields[i]];
if (!f) break;
}
return f ? `${acc} ${f}` : acc;
}, '');

View File

@ -72,7 +72,7 @@ export default class RelationControl extends React.Component {
if (value) {
const listValue = List.isList(value) ? value : List([value]);
listValue.forEach(val => {
const hit = hits.find(i => i.data[valueField] === val);
const hit = hits.find(i => this.parseNestedFields(i.data, valueField) === val);
if (hit) {
onChange(value, {
[field.get('name')]: {
@ -113,21 +113,38 @@ export default class RelationControl extends React.Component {
}
};
parseNestedFields = (targetObject, field) => {
let nestedField = field.split('.');
let f = targetObject;
for (let i = 0; i < nestedField.length; i++) {
f = f[nestedField[i]];
if (!f) break;
}
if (typeof f === 'object' && f !== null) {
return JSON.stringify(f);
}
return f;
};
parseHitOptions = hits => {
const { field } = this.props;
const valueField = field.get('valueField');
const displayField = field.get('displayFields') || field.get('valueField');
return hits.map(hit => {
let labelReturn;
if (List.isList(displayField)) {
labelReturn = displayField
.toJS()
.map(key => this.parseNestedFields(hit.data, key))
.join(' ');
} else {
labelReturn = this.parseNestedFields(hit.data, displayField);
}
return {
data: hit.data,
value: hit.data[valueField],
label: List.isList(displayField)
? displayField
.toJS()
.map(key => hit.data[key])
.join(' ')
: hit.data[displayField],
value: this.parseNestedFields(hit.data, valueField),
label: labelReturn,
};
});
};

View File

@ -16,6 +16,22 @@ const fieldConfig = {
valueField: 'title',
};
const deeplyNestedFieldConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug', 'deeply.nested.post.field'],
searchFields: ['deeply.nested.post.field'],
valueField: 'title',
};
const nestedFieldConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug', 'nested.field_1'],
searchFields: ['nested.field_1', 'nested.field_2'],
valueField: 'title',
};
const generateHits = length => {
const hits = Array.from({ length }, (val, idx) => {
const title = `Post # ${idx + 1}`;
@ -25,6 +41,31 @@ const generateHits = length => {
return [
...hits,
{
collection: 'posts',
data: {
title: 'Deeply nested post',
slug: 'post-deeply-nested',
deeply: {
nested: {
post: {
field: 'Deeply nested field',
},
},
},
},
},
{
collection: 'posts',
data: {
title: 'Nested post',
slug: 'post-nested',
nested: {
field_1: 'Nested field 1',
field_2: 'Nested field 2',
},
},
},
{
collection: 'posts',
data: { title: 'YAML post', slug: 'post-yaml', body: 'Body yaml' },
@ -51,6 +92,14 @@ class RelationController extends React.Component {
const queryHits = generateHits(25);
if (last(args) === 'YAML') {
return Promise.resolve({ payload: { response: { hits: [last(queryHits)] } } });
} else if (last(args) === 'Nested') {
return Promise.resolve({
payload: { response: { hits: [queryHits[queryHits.length - 2]] } },
});
} else if (last(args) === 'Deeply nested') {
return Promise.resolve({
payload: { response: { hits: [queryHits[queryHits.length - 3]] } },
});
}
return Promise.resolve({ payload: { response: { hits: queryHits } } });
});
@ -159,6 +208,28 @@ describe('Relation widget', () => {
});
});
it('should update option list based on nested search term', async () => {
const field = fromJS(nestedFieldConfig);
const { getAllByText, input } = setup({ field });
fireEvent.change(input, { target: { value: 'Nested' } });
await wait(() => {
expect(getAllByText('Nested post post-nested Nested field 1')).toHaveLength(1);
});
});
it('should update option list based on deeply nested search term', async () => {
const field = fromJS(deeplyNestedFieldConfig);
const { getAllByText, input } = setup({ field });
fireEvent.change(input, { target: { value: 'Deeply nested' } });
await wait(() => {
expect(
getAllByText('Deeply nested post post-deeply-nested Deeply nested field'),
).toHaveLength(1);
});
});
describe('with multiple', () => {
it('should call onChange with correct selectedItem value and metadata', async () => {
const field = fromJS({ ...fieldConfig, multiple: true });

View File

@ -11,18 +11,18 @@ The relation widget allows you to reference items from another collection. It pr
- **Options:**
- `default`: accepts any widget data type; defaults to an empty string
- `collection`: (**required**) name of the collection being referenced (string)
- `displayFields`: list of one or more names of fields in the referenced collection that will render in the autocomplete menu of the control. Defaults to `valueField`.
- `searchFields`: (**required**) list of one or more names of fields in the referenced collection to search for the typed value
- `valueField`: (**required**) name of the field from the referenced collection whose value will be stored for the relation
- `displayFields`: list of one or more names of fields in the referenced collection that will render in the autocomplete menu of the control. Defaults to `valueField`. For nested fields, separate each subfield with a `.` (E.g. `name.first`).
- `searchFields`: (**required**) list of one or more names of fields in the referenced collection to search for the typed value. Syntax to reference nested fields is similar to that of *displayFields*.
- `valueField`: (**required**) name of the field from the referenced collection whose value will be stored for the relation. Syntax to reference nested fields is similar to that of *displayFields* and *searchFields*.
- `multiple` : accepts a boolean, defaults to `false`
- **Example** (assuming a separate "authors" collection with "name" and "twitterHandle" fields):
- **Example** (assuming a separate "authors" collection with "name" and "twitterHandle" fields with subfields "first" and "last" for the "name" field):
```yaml
- label: "Post Author"
name: "author"
widget: "relation"
collection: "authors"
searchFields: ["name", "twitterHandle"]
valueField: "name"
searchFields: ["name.first", "twitterHandle"]
valueField: "name.first"
displayFields: ["twitterHandle", "followerCount"]
```
The generated UI input will search the authors collection by name and twitterHandle, and display each author's handle and follower count. On selection, the author name will be saved for the field.