feat(widget-relation): target file collections (#3754)
This commit is contained in:
parent
5f99a9132b
commit
2f435f875b
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Async as AsyncSelect } from 'react-select';
|
||||
import { find, isEmpty, last, debounce } from 'lodash';
|
||||
import { find, isEmpty, last, debounce, get, trimEnd } from 'lodash';
|
||||
import { List, Map, fromJS } from 'immutable';
|
||||
import { reactSelectStyles } from 'netlify-cms-ui-default';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
@ -35,6 +35,33 @@ function getSelectedValue({ value, options, isMultiple }) {
|
||||
}
|
||||
}
|
||||
|
||||
export const expandPath = ({ data, path, paths = [] }) => {
|
||||
if (path.endsWith('.*')) {
|
||||
path = path + '.';
|
||||
}
|
||||
|
||||
const sep = '.*.';
|
||||
const parts = path.split(sep);
|
||||
if (parts.length === 1) {
|
||||
paths.push(path);
|
||||
} else {
|
||||
const partialPath = parts[0];
|
||||
const value = get(data, partialPath);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v, index) => {
|
||||
expandPath({
|
||||
data,
|
||||
path: trimEnd(`${partialPath}.${index}.${parts.slice(1).join(sep)}`, '.'),
|
||||
paths,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
|
||||
export default class RelationControl extends React.Component {
|
||||
didInitialSearch = false;
|
||||
|
||||
@ -128,24 +155,26 @@ export default class RelationControl extends React.Component {
|
||||
parseHitOptions = hits => {
|
||||
const { field } = this.props;
|
||||
const valueField = field.get('valueField');
|
||||
const displayField = field.get('displayFields') || field.get('valueField');
|
||||
const displayField = field.get('displayFields') || List(field.get('valueField'));
|
||||
|
||||
return hits.map(hit => {
|
||||
let labelReturn;
|
||||
if (List.isList(displayField)) {
|
||||
labelReturn = displayField
|
||||
const options = hits.reduce((acc, hit) => {
|
||||
const valuesPaths = expandPath({ data: hit.data, path: valueField });
|
||||
for (let i = 0; i < valuesPaths.length; i++) {
|
||||
const label = displayField
|
||||
.toJS()
|
||||
.map(key => this.parseNestedFields(hit, key))
|
||||
.map(key => {
|
||||
const displayPaths = expandPath({ data: hit.data, path: key });
|
||||
return this.parseNestedFields(hit, displayPaths[i] || displayPaths[0]);
|
||||
})
|
||||
.join(' ');
|
||||
} else {
|
||||
labelReturn = this.parseNestedFields(hit, displayField);
|
||||
const value = this.parseNestedFields(hit, valuesPaths[i]);
|
||||
acc.push({ data: hit.data, value, label });
|
||||
}
|
||||
return {
|
||||
data: hit.data,
|
||||
value: this.parseNestedFields(hit, valueField),
|
||||
label: labelReturn,
|
||||
};
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
loadOptions = debounce((term, callback) => {
|
||||
@ -154,8 +183,13 @@ export default class RelationControl extends React.Component {
|
||||
const searchFields = field.get('searchFields');
|
||||
const optionsLength = field.get('optionsLength') || 20;
|
||||
const searchFieldsArray = List.isList(searchFields) ? searchFields.toJS() : [searchFields];
|
||||
const file = field.get('file');
|
||||
|
||||
query(forID, collection, searchFieldsArray, term).then(({ payload }) => {
|
||||
const queryPromise = file
|
||||
? query(forID, collection, ['slug'], file)
|
||||
: query(forID, collection, searchFieldsArray, term);
|
||||
|
||||
queryPromise.then(({ payload }) => {
|
||||
let options =
|
||||
payload.response && payload.response.hits
|
||||
? this.parseHitOptions(payload.response.hits)
|
||||
|
@ -3,6 +3,7 @@ import { fromJS, Map } from 'immutable';
|
||||
import { last } from 'lodash';
|
||||
import { render, fireEvent, wait } from '@testing-library/react';
|
||||
import { NetlifyCmsWidgetRelation } from '../';
|
||||
import { expandPath } from '../RelationControl';
|
||||
|
||||
const RelationControl = NetlifyCmsWidgetRelation.controlComponent;
|
||||
|
||||
@ -81,6 +82,27 @@ const generateHits = length => {
|
||||
];
|
||||
};
|
||||
|
||||
const simpleFileCollectionHits = [{ data: { categories: ['category 1', 'category 2'] } }];
|
||||
|
||||
const nestedFileCollectionHits = [
|
||||
{
|
||||
data: {
|
||||
nested: {
|
||||
categories: [
|
||||
{
|
||||
name: 'category 1',
|
||||
id: 'cat1',
|
||||
},
|
||||
{
|
||||
name: 'category 2',
|
||||
id: 'cat2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
class RelationController extends React.Component {
|
||||
state = {
|
||||
value: this.props.value,
|
||||
@ -98,7 +120,12 @@ class RelationController extends React.Component {
|
||||
|
||||
query = jest.fn((...args) => {
|
||||
const queryHits = generateHits(25);
|
||||
if (last(args) === 'YAML') {
|
||||
|
||||
if (last(args) === 'nested_file') {
|
||||
return Promise.resolve({ payload: { response: { hits: nestedFileCollectionHits } } });
|
||||
} else if (last(args) === 'simple_file') {
|
||||
return Promise.resolve({ payload: { response: { hits: simpleFileCollectionHits } } });
|
||||
} else if (last(args) === 'YAML') {
|
||||
return Promise.resolve({ payload: { response: { hits: [last(queryHits)] } } });
|
||||
} else if (last(args) === 'Nested') {
|
||||
return Promise.resolve({
|
||||
@ -160,6 +187,68 @@ function setup({ field, value }) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('expandPath', () => {
|
||||
it('should expand wildcard paths', () => {
|
||||
const data = {
|
||||
categories: [
|
||||
{
|
||||
name: 'category 1',
|
||||
},
|
||||
{
|
||||
name: 'category 2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(expandPath({ data, path: 'categories.*.name' })).toEqual([
|
||||
'categories.0.name',
|
||||
'categories.1.name',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle wildcard at the end of the path', () => {
|
||||
const data = {
|
||||
nested: {
|
||||
otherNested: {
|
||||
list: [
|
||||
{
|
||||
title: 'title 1',
|
||||
nestedList: [{ description: 'description 1' }, { description: 'description 2' }],
|
||||
},
|
||||
{
|
||||
title: 'title 2',
|
||||
nestedList: [{ description: 'description 2' }, { description: 'description 2' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(expandPath({ data, path: 'nested.otherNested.list.*.nestedList.*' })).toEqual([
|
||||
'nested.otherNested.list.0.nestedList.0',
|
||||
'nested.otherNested.list.0.nestedList.1',
|
||||
'nested.otherNested.list.1.nestedList.0',
|
||||
'nested.otherNested.list.1.nestedList.1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle non wildcard index', () => {
|
||||
const data = {
|
||||
categories: [
|
||||
{
|
||||
name: 'category 1',
|
||||
},
|
||||
{
|
||||
name: 'category 2',
|
||||
},
|
||||
],
|
||||
};
|
||||
const path = 'categories.0.name';
|
||||
|
||||
expect(expandPath({ data, path })).toEqual(['categories.0.name']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relation widget', () => {
|
||||
it('should list the first 20 option hits on initial load', async () => {
|
||||
const field = fromJS(fieldConfig);
|
||||
@ -319,4 +408,43 @@ describe('Relation widget', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with file collection', () => {
|
||||
const fileFieldConfig = {
|
||||
name: 'categories',
|
||||
collection: 'file',
|
||||
file: 'simple_file',
|
||||
valueField: 'categories.*',
|
||||
displayFields: ['categories.*'],
|
||||
};
|
||||
|
||||
it('should handle simple list', async () => {
|
||||
const field = fromJS(fileFieldConfig);
|
||||
const { getAllByText, input, getByText } = setup({ field });
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
|
||||
await wait(() => {
|
||||
expect(getAllByText(/category/)).toHaveLength(2);
|
||||
expect(getByText('category 1')).toBeInTheDocument();
|
||||
expect(getByText('category 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested list', async () => {
|
||||
const field = fromJS({
|
||||
...fileFieldConfig,
|
||||
file: 'nested_file',
|
||||
valueField: 'nested.categories.*.id',
|
||||
displayFields: ['nested.categories.*.name'],
|
||||
});
|
||||
const { getAllByText, input, getByText } = setup({ field });
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
|
||||
await wait(() => {
|
||||
expect(getAllByText(/category/)).toHaveLength(2);
|
||||
expect(getByText('category 1')).toBeInTheDocument();
|
||||
expect(getByText('category 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,8 +10,9 @@ The relation widget allows you to reference items from another collection. It pr
|
||||
- **Data type:** data type of the value pulled from the related collection item
|
||||
- **Options:**
|
||||
- `collection`: (**required**) name of the collection being referenced (string)
|
||||
- `valueField`: (**required**) name of the field from the referenced collection whose value will be stored for the relation. For nested fields, separate each subfield with a `.` (E.g. `name.first`).
|
||||
- `valueField`: (**required**) name of the field from the referenced collection whose value will be stored for the relation. For nested fields, separate each subfield with a `.` (e.g. `name.first`). For list fields use index notation to target a list item (e.g `categories.0`) or a wildcard `*` to target all list items (e.g. `categories.*`).
|
||||
- `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 *valueField*.
|
||||
- `file`: allows referencing a specific file when the collection being referenced is a files collection (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`. Syntax to reference nested fields is similar to that of *valueField*.
|
||||
- `default`: accepts any widget data type; defaults to an empty string
|
||||
- `multiple` : accepts a boolean, defaults to `false`
|
||||
|
Loading…
x
Reference in New Issue
Block a user