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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 409 additions and 144 deletions

View File

@ -34,8 +34,8 @@ collections: # A list of collections the CMS should be able to edit
label: 'Publish Date', label: 'Publish Date',
name: 'date', name: 'date',
widget: 'datetime', widget: 'datetime',
dateFormat: 'YYYY-MM-DD', date_format: 'YYYY-MM-DD',
timeFormat: 'HH:mm', time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm', format: 'YYYY-MM-DD HH:mm',
} }
- label: 'Cover Image' - label: 'Cover Image'
@ -108,9 +108,9 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
displayFields: ['title', 'date'] display_fields: ['title', 'date']
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'Title', name: 'title', widget: 'string' } - { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
- { label: 'Map', name: 'map', widget: 'map' } - { label: 'Map', name: 'map', widget: 'map' }
@ -139,8 +139,8 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'String', name: 'string', widget: 'string' } - { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
- { label: 'Text', name: 'text', widget: 'text' } - { label: 'Text', name: 'text', widget: 'text' }
@ -187,8 +187,8 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'String', name: 'string', widget: 'string' } - { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' } - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' } - { label: 'Text', name: 'text', widget: 'text' }

View File

@ -172,9 +172,9 @@
render: function() { render: function() {
// When a post is selected from the relation field, all of it's data // When a post is selected from the relation field, all of it's data
// will be available in the field's metadata nested under the collection // will be available in the field's metadata nested under the collection
// name, and then further nested under the value specified in `valueField`. // name, and then further nested under the value specified in `value_field`.
// In this case, the post would be nested under "posts" and then under // In this case, the post would be nested under "posts" and then under
// the title of the selected post, since our `valueField` in the config // the title of the selected post, since our `value_field` in the config
// is "title". // is "title".
const { value, fieldsMetaData } = this.props; const { value, fieldsMetaData } = this.props;
const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]); const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]);

View File

@ -34,8 +34,8 @@ collections: # A list of collections the CMS should be able to edit
label: 'Publish Date', label: 'Publish Date',
name: 'date', name: 'date',
widget: 'datetime', widget: 'datetime',
dateFormat: 'YYYY-MM-DD', date_format: 'YYYY-MM-DD',
timeFormat: 'HH:mm', time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm', format: 'YYYY-MM-DD HH:mm',
} }
- label: 'Cover Image' - label: 'Cover Image'
@ -108,9 +108,9 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
displayFields: ['title', 'date'] display_fields: ['title', 'date']
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'Title', name: 'title', widget: 'string' } - { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
- { label: 'Map', name: 'map', widget: 'map' } - { label: 'Map', name: 'map', widget: 'map' }
@ -139,8 +139,8 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'String', name: 'string', widget: 'string' } - { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
- { label: 'Text', name: 'text', widget: 'text' } - { label: 'Text', name: 'text', widget: 'text' }
@ -187,8 +187,8 @@ collections: # A list of collections the CMS should be able to edit
name: 'post' name: 'post'
widget: 'relationKitchenSinkPost' widget: 'relationKitchenSinkPost'
collection: 'posts' collection: 'posts'
searchFields: ['title', 'body'] search_fields: ['title', 'body']
valueField: 'title' value_field: 'title'
- { label: 'String', name: 'string', widget: 'string' } - { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' } - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' } - { label: 'Text', name: 'text', widget: 'text' }

View File

@ -172,9 +172,9 @@
render: function() { render: function() {
// When a post is selected from the relation field, all of it's data // When a post is selected from the relation field, all of it's data
// will be available in the field's metadata nested under the collection // will be available in the field's metadata nested under the collection
// name, and then further nested under the value specified in `valueField`. // name, and then further nested under the value specified in `value_field`.
// In this case, the post would be nested under "posts" and then under // In this case, the post would be nested under "posts" and then under
// the title of the selected post, since our `valueField` in the config // the title of the selected post, since our `value_field` in the config
// is "title". // is "title".
const { value, fieldsMetaData } = this.props; const { value, fieldsMetaData } = this.props;
const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]); const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]);

View File

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

View File

@ -1,8 +1,15 @@
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { stripIndent } from 'common-tags'; 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, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.mock('coreSrc/backend', () => { jest.mock('coreSrc/backend', () => {
return { return {
resolveBackend: jest.fn(() => ({ isGitBackend: jest.fn(() => true) })), 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', () => { describe('detectProxyServer', () => {

View File

@ -31,10 +31,79 @@ const setDefaultPublicFolder = map => {
return 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 = { const defaults = {
publish_mode: publishModes.SIMPLE, 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) { export function applyDefaults(config) {
return Map(defaults) return Map(defaults)
.mergeDeep(config) .mergeDeep(config)
@ -118,10 +187,10 @@ export function applyDefaults(config) {
); );
} }
if (!collection.has('sortableFields')) { if (!collection.has('sortable_fields')) {
const backend = resolveBackend(config); const backend = resolveBackend(config);
const defaultSortable = selectDefaultSortableFields(collection, backend); const defaultSortable = selectDefaultSortableFields(collection, backend);
collection = collection.set('sortableFields', fromJS(defaultSortable)); collection = collection.set('sortable_fields', fromJS(defaultSortable));
} }
if (!collection.has('view_filters')) { if (!collection.has('view_filters')) {
@ -283,7 +352,7 @@ export function loadConfig() {
mergedConfig = await handleLocalBackend(mergedConfig); mergedConfig = await handleLocalBackend(mergedConfig);
const config = applyDefaults(mergedConfig); const config = applyDefaults(normalizeConfig(mergedConfig));
dispatch(configDidLoad(config)); dispatch(configDidLoad(config));
dispatch(authenticateUser()); dispatch(authenticateUser());

View File

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

View File

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

View File

@ -183,24 +183,38 @@ describe('config', () => {
}).not.toThrowError(); }).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(() => { expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: 'title' }] })); validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: 'title' }] }));
}).toThrowError("'collections[0].sortableFields' should be array"); }).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(() => { expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: ['title'] }] })); validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: ['title'] }] }));
}).not.toThrow(); }).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(() => { expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: [] }] })); validateConfig(merge({}, validConfig, { collections: [{ sortableFields: [] }] }));
}).not.toThrow(); }).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', () => { it('should throw if collection names are not unique', () => {
expect(() => { expect(() => {
validateConfig( validateConfig(
@ -285,14 +299,14 @@ describe('config', () => {
name: 'relation', name: 'relation',
schema: { schema: {
properties: { properties: {
searchFields: { type: 'array', items: { type: 'string' } }, search_fields: { type: 'array', items: { type: 'string' } },
displayFields: { 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(() => { expect(() => {
validateConfig( validateConfig(
merge({}, validConfig, { merge({}, validConfig, {
@ -310,8 +324,8 @@ describe('config', () => {
name: 'relation', name: 'relation',
label: 'relation', label: 'relation',
widget: 'relation', widget: 'relation',
displayFields: 'title', display_fields: 'title',
searchFields: '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(() => { expect(() => {
validateConfig( validateConfig(
merge({}, validConfig, { merge({}, validConfig, {
@ -341,8 +355,8 @@ describe('config', () => {
name: 'relation', name: 'relation',
label: 'relation', label: 'relation',
widget: 'relation', widget: 'relation',
displayFields: ['title'], display_fields: ['title'],
searchFields: ['title'], search_fields: ['title'],
}, },
], ],
}, },

View File

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

View File

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

View File

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

View File

@ -36,9 +36,9 @@ export default class DateControl extends React.Component {
// dateFormat and timeFormat are strictly for modifying // dateFormat and timeFormat are strictly for modifying
// input field with the date/time pickers // 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 // 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') { if (typeof timeFormat === 'undefined') {
timeFormat = !!includeTime; timeFormat = !!includeTime;
} }

View File

@ -24,9 +24,9 @@ export default class DateTimeControl extends React.Component {
// dateFormat and timeFormat are strictly for modifying // dateFormat and timeFormat are strictly for modifying
// input field with the date/time pickers // 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 // 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') { if (typeof timeFormat === 'undefined') {
timeFormat = true; timeFormat = true;
} }
@ -46,7 +46,7 @@ export default class DateTimeControl extends React.Component {
getPickerUtc() { getPickerUtc() {
const { field } = this.props; const { field } = this.props;
const pickerUtc = field.get('pickerUtc'); const pickerUtc = field.get('picker_utc');
return pickerUtc; return pickerUtc;
} }

View File

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

View File

@ -195,7 +195,7 @@ export default class Editor extends React.Component {
onAddAsset={onAddAsset} onAddAsset={onAddAsset}
getAsset={getAsset} getAsset={getAsset}
buttons={field.get('buttons')} buttons={field.get('buttons')}
editorComponents={field.get('editorComponents')} editorComponents={field.get('editor_components')}
hasMark={this.hasMark} hasMark={this.hasMark}
hasInline={this.hasInline} hasInline={this.hasInline}
hasBlock={this.hasBlock} 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 => { handleChange = e => {
const valueType = this.props.field.get('valueType'); const valueType = this.props.field.get('value_type');
const { onChange } = this.props; const { onChange } = this.props;
const value = valueType === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10); 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 { field, value, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props;
const min = field.get('min', ''); const min = field.get('min', '');
const max = field.get('max', ''); 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 ( return (
<input <input
type="number" type="number"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -209,7 +209,7 @@ The `collections` setting is the heart of your Netlify CMS configuration, as it
* `fields` (required): see detailed description below * `fields` (required): see detailed description below
* `editor`: see detailed description below * `editor`: see detailed description below
* `summary`: see detailed description below * `summary`: see detailed description below
* `sortableFields`: see detailed description below * `sortable_fields`: see detailed description below
* `view_filters`: see detailed description below * `view_filters`: see detailed description below
The last few options require more detailed information. The last few options require more detailed information.
@ -384,7 +384,7 @@ Template tags are the same as those for [slug](#slug), with the following additi
summary: "Version: {{version}} - {{title}}" summary: "Version: {{version}} - {{title}}"
``` ```
### `sortableFields` ### `sortable_fields`
An optional list of sort fields to show in the UI. An optional list of sort fields to show in the UI.
@ -396,7 +396,7 @@ When `author` field can't be inferred commit author will be used.
```yaml ```yaml
# use dot notation for nested fields # use dot notation for nested fields
sortableFields: ['commit_date', 'title', 'commit_author', 'language.en'] sortable_fields: ['commit_date', 'title', 'commit_author', 'language.en']
``` ```
### `view_filters` ### `view_filters`

View File

@ -201,9 +201,9 @@ fields:
name: 'author', name: 'author',
widget: 'relation', widget: 'relation',
collection: 'authors', collection: 'authors',
displayFields: [display_name], display_fields: [display_name],
searchFields: [display_name], search_fields: [display_name],
valueField: 'name', value_field: 'name',
} }
- { label: 'Body', name: 'body', widget: 'markdown' } - { label: 'Body', name: 'body', widget: 'markdown' }
``` ```

View File

@ -13,8 +13,8 @@ The date widget translates a date picker input to a date string. For saving date
- **Options:** - **Options:**
- `default`: accepts a date string, or an empty string to accept blank input; otherwise defaults to current date - `default`: accepts a date string, or an empty string to accept blank input; otherwise defaults to current date
- `format`: optional; accepts Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/); defaults to raw Date object (if supported by output format) - `format`: optional; accepts Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/); defaults to raw Date object (if supported by output format)
- `dateFormat`: optional; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format. - `date_format`: optional; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format.
- `timeFormat`: optional; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format, `false` hides time-picker. Defaults to false. - `time_format`: optional; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format, `false` hides time-picker. Defaults to false.
- **Example:** - **Example:**
```yaml ```yaml
- label: 'Birthdate' - label: 'Birthdate'

View File

@ -11,17 +11,17 @@ The datetime widget translates a datetime picker to a datetime string.
- **Options:** - **Options:**
- `default`: accepts a datetime string, or an empty string to accept blank input; otherwise defaults to current datetime - `default`: accepts a datetime string, or an empty string to accept blank input; otherwise defaults to current datetime
- `format`: sets storage format; accepts Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/); defaults to raw Date object (if supported by output format) - `format`: sets storage format; accepts Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/); defaults to raw Date object (if supported by output format)
- `dateFormat`: sets date display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format. - `date_format`: sets date display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format.
- `timeFormat`: sets time display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format, `false` hides time-picker. - `time_format`: sets time display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format, `false` hides time-picker.
- `pickerUtc`: _(default: `false`)_ when set to `true`, the datetime picker will display times in UTC. When `false`, the datetime picker will display times in the user's local timezone. When using date-only formats, it can be helpful to set this to `true` so users in all timezones will see the same date in the datetime picker. - `picker_utc`: _(default: `false`)_ when set to `true`, the datetime picker will display times in UTC. When `false`, the datetime picker will display times in the user's local timezone. When using date-only formats, it can be helpful to set this to `true` so users in all timezones will see the same date in the datetime picker.
- **Example:** - **Example:**
```yaml ```yaml
- label: "Start time" - label: "Start time"
name: "start" name: "start"
widget: "datetime" widget: "datetime"
default: "" default: ""
dateFormat: "DD.MM.YYYY" # e.g. 24.12.2021 date_format: "DD.MM.YYYY" # e.g. 24.12.2021
timeFormat: "HH:mm" # e.g. 21:07 time_format: "HH:mm" # e.g. 21:07
format: "LLL" format: "LLL"
pickerUtc: false picker_utc: false
``` ```

View File

@ -14,7 +14,7 @@ _Please note:_ If you want to use your markdown editor to fill a markdown file c
- `default`: accepts markdown content - `default`: accepts markdown content
- `minimal`: accepts a boolean value, `false` by default. Sets the widget height to minimum possible. - `minimal`: accepts a boolean value, `false` by default. Sets the widget height to minimum possible.
- `buttons`: an array of strings representing the formatting buttons to display (all shown by default). Buttons include: `bold`, `italic`, `code`, `link`, `heading-one`, `heading-two`, `heading-three`, `heading-four`, `heading-five`, `heading-six`, `quote`, `bulleted-list`, and `numbered-list`. - `buttons`: an array of strings representing the formatting buttons to display (all shown by default). Buttons include: `bold`, `italic`, `code`, `link`, `heading-one`, `heading-two`, `heading-three`, `heading-four`, `heading-five`, `heading-six`, `quote`, `bulleted-list`, and `numbered-list`.
- `editorComponents`: an array of strings representing the names of editor components to display (all shown by default). The `image` and `code-block` editor components are included with Netlify CMS by default, but others may be [created and registered](/docs/custom-widgets/#registereditorcomponent). - `editor_components`: an array of strings representing the names of editor components to display (all shown by default). The `image` and `code-block` editor components are included with Netlify CMS by default, but others may be [created and registered](/docs/custom-widgets/#registereditorcomponent).
- **Example:** - **Example:**
```yaml ```yaml

View File

@ -7,10 +7,10 @@ The number widget uses an HTML number input, saving the value as a string, integ
- **Name:** `number` - **Name:** `number`
- **UI:** HTML [number input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number) - **UI:** HTML [number input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number)
- **Data type:** string by default; configured by `valueType` option - **Data type:** string by default; configured by `value_type` option
- **Options:** - **Options:**
- `default`: accepts string or number value; defaults to empty string - `default`: accepts string or number value; defaults to empty string
- `valueType`: accepts `int` or `float`; any other value results in saving as a string - `value_type`: accepts `int` or `float`; any other value results in saving as a string
- `min`: accepts a number for minimum value accepted; unset by default - `min`: accepts a number for minimum value accepted; unset by default
- `max`: accepts a number for maximum value accepted; unset by default - `max`: accepts a number for maximum value accepted; unset by default
- `step`: accepts a number for stepping up/down values in the input; 1 by default - `step`: accepts a number for stepping up/down values in the input; 1 by default
@ -20,7 +20,7 @@ The number widget uses an HTML number input, saving the value as a string, integ
name: "puppies" name: "puppies"
widget: "number" widget: "number"
default: 2 default: 2
valueType: "int" value_type: "int"
min: 1 min: 1
max: 101 max: 101
step: 2 step: 2

View File

@ -10,13 +10,13 @@ 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 - **Data type:** data type of the value pulled from the related collection item
- **Options:** - **Options:**
- `collection`: (**required**) name of the collection being referenced (string) - `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`). For list fields use a wildcard `*` to target all list items (e.g. `categories.*`). - `value_field`: (**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 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*. - `search_fields`: (**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 *value_field*.
- `file`: allows referencing a specific file when the collection being referenced is a files collection (string) - `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*. - `display_fields`: list of one or more names of fields in the referenced collection that will render in the autocomplete menu of the control. Defaults to `value_field`. Syntax to reference nested fields is similar to that of *value_field*.
- `default`: accepts any widget data type; defaults to an empty string - `default`: accepts any widget data type; defaults to an empty string
- `multiple` : accepts a boolean, defaults to `false` - `multiple` : accepts a boolean, defaults to `false`
- `optionsLength`: accepts integer to override number of options presented to user. Defaults to `20`. - `options_length`: accepts integer to override number of options presented to user. Defaults to `20`.
- **Referencing a folder collection example** (assuming a separate "authors" collection with "name" and "twitterHandle" fields with subfields "first" and "last" for the "name" field): - **Referencing a folder collection example** (assuming a separate "authors" collection with "name" and "twitterHandle" fields with subfields "first" and "last" for the "name" field):
```yaml ```yaml
@ -24,9 +24,9 @@ The relation widget allows you to reference items from another collection. It pr
name: "author" name: "author"
widget: "relation" widget: "relation"
collection: "authors" collection: "authors"
searchFields: ["name.first", "twitterHandle"] search_fields: ["name.first", "twitterHandle"]
valueField: "name.first" value_field: "name.first"
displayFields: ["twitterHandle", "followerCount"] display_fields: ["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. 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.
@ -38,9 +38,9 @@ The generated UI input will search the authors collection by name and twitterHan
name: "author" name: "author"
widget: "relation" widget: "relation"
collection: "authors" collection: "authors"
searchFields: ['name.first'] search_fields: ['name.first']
valueField: "{{slug}}" value_field: "{{slug}}"
displayFields: ["{{twitterHandle}} - {{followerCount}}"] display_fields: ["{{twitterHandle}} - {{followerCount}}"]
``` ```
The generated UI input will search the authors collection by name, and display each author's handle and follower count. On selection, the author entry slug will be saved for the field. The generated UI input will search the authors collection by name, and display each author's handle and follower count. On selection, the author entry slug will be saved for the field.
@ -53,9 +53,9 @@ The generated UI input will search the authors collection by name, and display e
widget: "relation" widget: "relation"
collection: "relation_files" collection: "relation_files"
file: "cities" file: "cities"
searchFields: ["cities.*.name"] search_fields: ["cities.*.name"]
displayFields: ["cities.*.name"] display_fields: ["cities.*.name"]
valueField: "cities.*.id" value_field: "cities.*.id"
``` ```
The generated UI input will search the cities file by city name, and display each city's name. On selection, the city id will be saved for the field. The generated UI input will search the cities file by city name, and display each city's name. On selection, the city id will be saved for the field.