fix: deprecate inconsistent config param case (#4172)

This commit is contained in:
andreascm
2020-08-31 19:25:48 +08:00
committed by GitHub
parent f1376aa5c3
commit 88a5a8098e
31 changed files with 409 additions and 144 deletions

View File

@ -61,8 +61,21 @@ declare module 'netlify-cms-core' {
/** If widget === "datetime" */
format?: string;
date_format?: boolean | string;
time_format?: boolean | string;
picker_utc?: boolean;
/**
* @deprecated Use date_format instead
*/
dateFormat?: boolean | string;
/**
* @deprecated Use time_format instead
*/
timeFormat?: boolean | string;
/**
* @deprecated Use picker_utc instead
*/
pickerUtc?: boolean;
/** If widget === "file" || widget === "image" */
@ -89,12 +102,22 @@ declare module 'netlify-cms-core' {
/** If widget === "markdown" */
minimal?: boolean;
buttons?: CmsMarkdownWidgetButton[];
editor_components?: string[];
/**
* @deprecated Use editor_components instead
*/
editorComponents?: string[];
/** If widget === "number" */
valueType?: 'int' | 'float' | string;
value_type?: 'int' | 'float' | string;
step?: number;
/**
* @deprecated Use valueType instead
*/
valueType?: 'int' | 'float' | string;
/** If widget === "number" || widget === "select" */
min?: number;
max?: number;
@ -104,10 +127,27 @@ declare module 'netlify-cms-core' {
/** If widget === "relation" */
collection?: string;
valueField?: string;
searchFields?: string[];
value_field?: string;
search_fields?: string[];
file?: string;
display_fields?: string[];
options_length?: number;
/**
* @deprecated Use value_field instead
*/
valueField?: string;
/**
* @deprecated Use search_fields instead
*/
searchFields?: string[];
/**
* @deprecated Use display_fields instead
*/
displayFields?: string[];
/**
* @deprecated Use options_length instead
*/
optionsLength?: number;
/** If widget === "select" */
@ -155,6 +195,11 @@ declare module 'netlify-cms-core' {
path?: string;
media_folder?: string;
public_folder?: string;
sortable_fields?: string[];
/**
* @deprecated Use sortable_fields instead
*/
sortableFields?: string[];
}

View File

@ -1,8 +1,15 @@
import { fromJS } from 'immutable';
import { stripIndent } from 'common-tags';
import { parseConfig, applyDefaults, detectProxyServer, handleLocalBackend } from '../config';
import {
parseConfig,
normalizeConfig,
applyDefaults,
detectProxyServer,
handleLocalBackend,
} from '../config';
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.mock('coreSrc/backend', () => {
return {
resolveBackend: jest.fn(() => ({ isGitBackend: jest.fn(() => true) })),
@ -413,6 +420,119 @@ describe('config', () => {
});
});
});
test('should convert camel case to snake case', () => {
expect(
applyDefaults(
normalizeConfig(
fromJS({
collections: [
{
sortableFields: ['title'],
folder: 'src',
identifier_field: 'datetime',
fields: [
{
name: 'datetime',
widget: 'datetime',
dateFormat: 'YYYY/MM/DD',
timeFormat: 'HH:mm',
pickerUtc: true,
},
{
widget: 'number',
valueType: 'float',
},
],
},
{
sortableFields: [],
files: [
{
name: 'file',
file: 'src/file.json',
fields: [
{
widget: 'markdown',
editorComponents: ['code'],
},
{
widget: 'relation',
valueField: 'title',
searchFields: ['title'],
displayFields: ['title'],
optionsLength: 5,
},
],
},
],
},
],
}),
),
).toJS(),
).toEqual({
public_folder: '/',
publish_mode: 'simple',
slug: { clean_accents: false, encoding: 'unicode', sanitize_replacement: '-' },
collections: [
{
sortable_fields: ['title'],
folder: 'src',
identifier_field: 'datetime',
fields: [
{
name: 'datetime',
widget: 'datetime',
date_format: 'YYYY/MM/DD',
dateFormat: 'YYYY/MM/DD',
time_format: 'HH:mm',
timeFormat: 'HH:mm',
picker_utc: true,
pickerUtc: true,
},
{
widget: 'number',
value_type: 'float',
valueType: 'float',
},
],
meta: {},
publish: true,
view_filters: [],
},
{
sortable_fields: [],
files: [
{
name: 'file',
file: 'src/file.json',
fields: [
{
widget: 'markdown',
editor_components: ['code'],
editorComponents: ['code'],
},
{
widget: 'relation',
value_field: 'title',
valueField: 'title',
search_fields: ['title'],
searchFields: ['title'],
display_fields: ['title'],
displayFields: ['title'],
options_length: 5,
optionsLength: 5,
},
],
},
],
publish: true,
view_filters: [],
},
],
});
});
});
describe('detectProxyServer', () => {

View File

@ -31,10 +31,79 @@ const setDefaultPublicFolder = map => {
return map;
};
const setSnakeCaseConfig = field => {
// Mapping between existing camelCase and its snake_case counterpart
const widgetKeyMap = {
dateFormat: 'date_format',
timeFormat: 'time_format',
pickerUtc: 'picker_utc',
editorComponents: 'editor_components',
valueType: 'value_type',
valueField: 'value_field',
searchFields: 'search_fields',
displayFields: 'display_fields',
optionsLength: 'options_length',
};
Object.entries(widgetKeyMap).forEach(([camel, snake]) => {
if (field.has(camel)) {
field = field.set(snake, field.get(camel));
console.warn(
`Field ${field.get(
'name',
)} is using a deprecated configuration '${camel}'. Please use '${snake}'`,
);
}
});
return field;
};
const defaults = {
publish_mode: publishModes.SIMPLE,
};
export function normalizeConfig(config) {
return Map(config).withMutations(map => {
map.set(
'collections',
map.get('collections').map(collection => {
const folder = collection.get('folder');
if (folder) {
collection = collection.set(
'fields',
traverseFields(collection.get('fields'), setSnakeCaseConfig),
);
}
const files = collection.get('files');
if (files) {
collection = collection.set(
'files',
files.map(file => {
file = file.set('fields', traverseFields(file.get('fields'), setSnakeCaseConfig));
return file;
}),
);
}
if (collection.has('sortableFields')) {
collection = collection
.set('sortable_fields', collection.get('sortableFields'))
.delete('sortableFields');
console.warn(
`Collection ${collection.get(
'name',
)} is using a deprecated configuration 'sortableFields'. Please use 'sortable_fields'`,
);
}
return collection;
}),
);
});
}
export function applyDefaults(config) {
return Map(defaults)
.mergeDeep(config)
@ -118,10 +187,10 @@ export function applyDefaults(config) {
);
}
if (!collection.has('sortableFields')) {
if (!collection.has('sortable_fields')) {
const backend = resolveBackend(config);
const defaultSortable = selectDefaultSortableFields(collection, backend);
collection = collection.set('sortableFields', fromJS(defaultSortable));
collection = collection.set('sortable_fields', fromJS(defaultSortable));
}
if (!collection.has('view_filters')) {
@ -283,7 +352,7 @@ export function loadConfig() {
mergedConfig = await handleLocalBackend(mergedConfig);
const config = applyDefaults(mergedConfig);
const config = applyDefaults(normalizeConfig(mergedConfig));
dispatch(configDidLoad(config));
dispatch(authenticateUser());

View File

@ -21,7 +21,7 @@ const renderWithRedux = (component, { store } = {}) => {
};
describe('Collection', () => {
const collection = fromJS({ name: 'pages', sortableFields: [], view_filters: [] });
const collection = fromJS({ name: 'pages', sortable_fields: [], view_filters: [] });
const props = {
collections: fromJS([collection]).toOrderedMap(),
collection,

View File

@ -14,8 +14,8 @@ exports[`Collection should render connected component 1`] = `
class="emotion-2 emotion-3"
>
<mock-sidebar
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] } }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] } }"
filterterm=""
searchterm=""
/>
@ -23,7 +23,7 @@ exports[`Collection should render connected component 1`] = `
class="emotion-0 emotion-1"
>
<mock-collection-top
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] }"
newentryurl=""
/>
<mock-collection-controls
@ -32,7 +32,7 @@ exports[`Collection should render connected component 1`] = `
viewfilters=""
/>
<mock-entries-collection
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] }"
filterterm=""
/>
</main>
@ -54,19 +54,19 @@ exports[`Collection should render with collection with create url 1`] = `
class="emotion-2 emotion-3"
>
<mock-sidebar
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] } }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] } }"
/>
<main
class="emotion-0 emotion-1"
>
<mock-collection-top
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
newentryurl="/collections/pages/new"
/>
<mock-collection-controls />
<mock-entries-collection
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
/>
</main>
</div>
@ -87,20 +87,20 @@ exports[`Collection should render with collection with create url and path 1`] =
class="emotion-2 emotion-3"
>
<mock-sidebar
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] } }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] } }"
filterterm="dir1/dir2"
/>
<main
class="emotion-0 emotion-1"
>
<mock-collection-top
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
newentryurl="/collections/pages/new?path=dir1/dir2"
/>
<mock-collection-controls />
<mock-entries-collection
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": true }"
filterterm="dir1/dir2"
/>
</main>
@ -122,19 +122,19 @@ exports[`Collection should render with collection without create url 1`] = `
class="emotion-2 emotion-3"
>
<mock-sidebar
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [] } }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
collections="OrderedMap { 0: Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [] } }"
/>
<main
class="emotion-0 emotion-1"
>
<mock-collection-top
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
newentryurl=""
/>
<mock-collection-controls />
<mock-entries-collection
collection="Map { \\"name\\": \\"pages\\", \\"sortableFields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
collection="Map { \\"name\\": \\"pages\\", \\"sortable_fields\\": List [], \\"view_filters\\": List [], \\"create\\": false }"
/>
</main>
</div>

View File

@ -183,24 +183,38 @@ describe('config', () => {
}).not.toThrowError();
});
it('should throw if collections sortableFields is not a boolean or a string array', () => {
it('should throw if collections sortable_fields is not a boolean or a string array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: 'title' }] }));
}).toThrowError("'collections[0].sortableFields' should be array");
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: 'title' }] }));
}).toThrowError("'collections[0].sortable_fields' should be array");
});
it('should allow sortableFields to be a string array', () => {
it('should allow sortable_fields to be a string array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: ['title'] }] }));
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: ['title'] }] }));
}).not.toThrow();
});
it('should allow sortableFields to be a an empty array', () => {
it('should allow sortable_fields to be a an empty array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: [] }] }));
}).not.toThrow();
});
it('should allow sortableFields instead of sortable_fields', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: [] }] }));
}).not.toThrow();
});
it('should throw if both sortable_fields and sortableFields exist', () => {
expect(() => {
validateConfig(
merge({}, validConfig, { collections: [{ sortable_fields: [], sortableFields: [] }] }),
);
}).toThrowError("'collections[0]' should NOT be valid");
});
it('should throw if collection names are not unique', () => {
expect(() => {
validateConfig(
@ -285,14 +299,14 @@ describe('config', () => {
name: 'relation',
schema: {
properties: {
searchFields: { type: 'array', items: { type: 'string' } },
displayFields: { type: 'array', items: { type: 'string' } },
search_fields: { type: 'array', items: { type: 'string' } },
display_fields: { type: 'array', items: { type: 'string' } },
},
},
},
]);
it('should throw if nested relation displayFields and searchFields are not arrays', () => {
it('should throw if nested relation display_fields and search_fields are not arrays', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
@ -310,8 +324,8 @@ describe('config', () => {
name: 'relation',
label: 'relation',
widget: 'relation',
displayFields: 'title',
searchFields: 'title',
display_fields: 'title',
search_fields: 'title',
},
],
},
@ -320,10 +334,10 @@ describe('config', () => {
],
}),
);
}).toThrowError("'searchFields' should be array\n'displayFields' should be array");
}).toThrowError("'search_fields' should be array\n'display_fields' should be array");
});
it('should not throw if nested relation displayFields and searchFields are arrays', () => {
it('should not throw if nested relation display_fields and search_fields are arrays', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
@ -341,8 +355,8 @@ describe('config', () => {
name: 'relation',
label: 'relation',
widget: 'relation',
displayFields: ['title'],
searchFields: ['title'],
display_fields: ['title'],
search_fields: ['title'],
},
],
},

View File

@ -181,6 +181,12 @@ const getConfigSchema = () => ({
},
},
fields: fieldsConfig(),
sortable_fields: {
type: 'array',
items: {
type: 'string',
},
},
sortableFields: {
type: 'array',
items: {
@ -215,6 +221,9 @@ const getConfigSchema = () => ({
},
required: ['name', 'label'],
oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }],
not: {
required: ['sortable_fields', 'sortableFields'],
},
if: { required: ['extension'] },
then: {
// Cannot infer format from extension.

View File

@ -245,6 +245,7 @@ export const traverseFields = (
if (done()) {
return fields;
}
fields = fields
.map(f => {
const field = updater(f as EntryField);
@ -395,7 +396,7 @@ export const selectDefaultSortableFields = (collection: Collection, backend: Bac
export const selectSortableFields = (collection: Collection, t: (key: string) => string) => {
const fields = collection
.get('sortableFields')
.get('sortable_fields')
.toArray()
.map(key => {
if (key === COMMIT_DATE) {

View File

@ -183,7 +183,7 @@ type CollectionObject = {
slug?: string;
label_singular?: string;
label: string;
sortableFields: List<string>;
sortable_fields: List<string>;
view_filters: List<StaticallyTypedRecord<ViewFilter>>;
nested?: Nested;
meta?: Meta;

View File

@ -36,9 +36,9 @@ export default class DateControl extends React.Component {
// dateFormat and timeFormat are strictly for modifying
// input field with the date/time pickers
const dateFormat = field.get('dateFormat');
const dateFormat = field.get('date_format');
// show time-picker? false hides it, true shows it using default format
let timeFormat = field.get('timeFormat');
let timeFormat = field.get('time_format');
if (typeof timeFormat === 'undefined') {
timeFormat = !!includeTime;
}

View File

@ -24,9 +24,9 @@ export default class DateTimeControl extends React.Component {
// dateFormat and timeFormat are strictly for modifying
// input field with the date/time pickers
const dateFormat = field.get('dateFormat');
const dateFormat = field.get('date_format');
// show time-picker? false hides it, true shows it using default format
let timeFormat = field.get('timeFormat');
let timeFormat = field.get('time_format');
if (typeof timeFormat === 'undefined') {
timeFormat = true;
}
@ -46,7 +46,7 @@ export default class DateTimeControl extends React.Component {
getPickerUtc() {
const { field } = this.props;
const pickerUtc = field.get('pickerUtc');
const pickerUtc = field.get('picker_utc');
return pickerUtc;
}

View File

@ -1,8 +1,8 @@
export default {
properties: {
format: { type: 'string' },
dateFormat: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
timeFormat: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
pickerUtc: { type: 'boolean' },
date_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
time_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
picker_utc: { type: 'boolean' },
},
};

View File

@ -195,7 +195,7 @@ export default class Editor extends React.Component {
onAddAsset={onAddAsset}
getAsset={getAsset}
buttons={field.get('buttons')}
editorComponents={field.get('editorComponents')}
editorComponents={field.get('editor_components')}
hasMark={this.hasMark}
hasInline={this.hasInline}
hasBlock={this.hasBlock}

View File

@ -22,6 +22,6 @@ export default {
],
},
},
editorComponents: { type: 'array', items: { type: 'string' } },
editor_components: { type: 'array', items: { type: 'string' } },
},
};

View File

@ -69,7 +69,7 @@ export default class NumberControl extends React.Component {
};
handleChange = e => {
const valueType = this.props.field.get('valueType');
const valueType = this.props.field.get('value_type');
const { onChange } = this.props;
const value = valueType === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10);
@ -99,7 +99,7 @@ export default class NumberControl extends React.Component {
const { field, value, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props;
const min = field.get('min', '');
const max = field.get('max', '');
const step = field.get('step', field.get('valueType') === 'int' ? 1 : '');
const step = field.get('step', field.get('value_type') === 'int' ? 1 : '');
return (
<input
type="number"

View File

@ -10,7 +10,7 @@ const fieldSettings = {
min: -20,
max: 20,
step: 1,
valueType: 'int',
value_type: 'int',
};
class NumberController extends React.Component {
@ -120,7 +120,7 @@ describe('Number widget', () => {
});
it('should parse float numbers as float', () => {
const field = fromJS({ ...fieldSettings, valueType: 'float' });
const field = fromJS({ ...fieldSettings, value_type: 'float' });
const testValue = (Math.random() * (20 - -20 + 1) + -20).toFixed(2);
const { input, onChangeSpy } = setup({ field });

View File

@ -1,7 +1,7 @@
export default {
properties: {
step: { type: 'number' },
valueType: { type: 'string' },
value_type: { type: 'string' },
min: { type: 'number' },
max: { type: 'number' },
},

View File

@ -113,7 +113,7 @@ export default class RelationControl extends React.Component {
const metadata = {};
const allOptions = await Promise.all(
initialSearchValues.map((v, index) => {
return query(forID, collection, [field.get('valueField')], v, file, 1).then(
return query(forID, collection, [field.get('value_field')], v, file, 1).then(
({ payload }) => {
const hits = payload.response?.hits || [];
const options = this.parseHitOptions(hits);
@ -189,8 +189,8 @@ export default class RelationControl extends React.Component {
parseHitOptions = hits => {
const { field } = this.props;
const valueField = field.get('valueField');
const displayField = field.get('displayFields') || List([field.get('valueField')]);
const valueField = field.get('value_field');
const displayField = field.get('display_fields') || List([field.get('value_field')]);
const options = hits.reduce((acc, hit) => {
const valuesPaths = stringTemplate.expandPath({ data: hit.data, path: valueField });
for (let i = 0; i < valuesPaths.length; i++) {
@ -214,8 +214,8 @@ export default class RelationControl extends React.Component {
loadOptions = debounce((term, callback) => {
const { field, query, forID } = this.props;
const collection = field.get('collection');
const searchFields = field.get('searchFields');
const optionsLength = field.get('optionsLength') || 20;
const searchFields = field.get('search_fields');
const optionsLength = field.get('options_length') || 20;
const searchFieldsArray = List.isList(searchFields) ? searchFields.toJS() : [searchFields];
const file = field.get('file');

View File

@ -15,34 +15,34 @@ const RelationControl = NetlifyCmsWidgetRelation.controlComponent;
const fieldConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug'],
searchFields: ['title', 'body'],
valueField: 'title',
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
};
const customizedOptionsLengthConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug'],
searchFields: ['title', 'body'],
valueField: 'title',
optionsLength: 10,
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
options_length: 10,
};
const deeplyNestedFieldConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug', 'deeply.nested.post.field'],
searchFields: ['deeply.nested.post.field'],
valueField: 'title',
display_fields: ['title', 'slug', 'deeply.nested.post.field'],
search_fields: ['deeply.nested.post.field'],
value_field: 'title',
};
const nestedFieldConfig = {
name: 'post',
collection: 'posts',
displayFields: ['title', 'slug', 'nested.field_1'],
searchFields: ['nested.field_1', 'nested.field_2'],
valueField: 'title',
display_fields: ['title', 'slug', 'nested.field_1'],
search_fields: ['nested.field_1', 'nested.field_2'],
value_field: 'title',
};
const generateHits = length => {
@ -327,9 +327,9 @@ describe('Relation widget', () => {
const stringTemplateConfig = {
name: 'post',
collection: 'posts',
displayFields: ['{{slug}}', '{{filename}}', '{{extension}}'],
searchFields: ['slug'],
valueField: '{{slug}}',
display_fields: ['{{slug}}', '{{filename}}', '{{extension}}'],
search_fields: ['slug'],
value_field: '{{slug}}',
};
const field = fromJS(stringTemplateConfig);
@ -348,8 +348,8 @@ describe('Relation widget', () => {
});
});
it('should default displayFields to valueField', async () => {
const field = fromJS(fieldConfig).delete('displayFields');
it('should default display_fields to value_field', async () => {
const field = fromJS(fieldConfig).delete('display_fields');
const { getAllByText, input } = setup({ field });
fireEvent.keyDown(input, { key: 'ArrowDown' });
@ -361,9 +361,9 @@ describe('Relation widget', () => {
const fieldConfig = {
name: 'numbers',
collection: 'numbers_collection',
valueField: 'index',
searchFields: ['index'],
displayFields: ['title'],
value_field: 'index',
search_fields: ['index'],
display_fields: ['title'],
};
const field = fromJS(fieldConfig);
@ -442,8 +442,8 @@ describe('Relation widget', () => {
name: 'categories',
collection: 'file',
file: 'simple_file',
valueField: 'categories.*',
displayFields: ['categories.*'],
value_field: 'categories.*',
display_fields: ['categories.*'],
};
it('should handle simple list', async () => {
@ -462,8 +462,8 @@ describe('Relation widget', () => {
const field = fromJS({
...fileFieldConfig,
file: 'nested_file',
valueField: 'nested.categories.*.id',
displayFields: ['nested.categories.*.name'],
value_field: 'nested.categories.*.id',
display_fields: ['nested.categories.*.name'],
});
const { getAllByText, input, getByText } = setup({ field });
fireEvent.keyDown(input, { key: 'ArrowDown' });

View File

@ -1,12 +1,19 @@
export default {
properties: {
collection: { type: 'string' },
valueField: { type: 'string' },
searchFields: { type: 'array', minItems: 1, items: { type: 'string' } },
value_field: { type: 'string' },
search_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
file: { type: 'string' },
multiple: { type: 'boolean' },
displayFields: { type: 'array', minItems: 1, items: { type: 'string' } },
optionsLength: { type: 'integer' },
display_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
options_length: { type: 'integer' },
},
required: ['collection', 'valueField', 'searchFields'],
oneOf: [
{
required: ['collection', 'value_field', 'search_fields'],
},
{
required: ['collection', 'valueField', 'searchFields'],
},
],
};