diff --git a/dev-test/backends/test/config.yml b/dev-test/backends/test/config.yml index 1b30f19d..b9a3f720 100644 --- a/dev-test/backends/test/config.yml +++ b/dev-test/backends/test/config.yml @@ -34,8 +34,8 @@ collections: # A list of collections the CMS should be able to edit label: 'Publish Date', name: 'date', widget: 'datetime', - dateFormat: 'YYYY-MM-DD', - timeFormat: 'HH:mm', + date_format: 'YYYY-MM-DD', + time_format: 'HH:mm', format: 'YYYY-MM-DD HH:mm', } - label: 'Cover Image' @@ -108,9 +108,9 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - displayFields: ['title', 'date'] - searchFields: ['title', 'body'] - valueField: 'title' + display_fields: ['title', 'date'] + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'Title', name: 'title', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true } - { label: 'Map', name: 'map', widget: 'map' } @@ -139,8 +139,8 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - searchFields: ['title', 'body'] - valueField: 'title' + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'String', name: 'string', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false } - { label: 'Text', name: 'text', widget: 'text' } @@ -187,8 +187,8 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - searchFields: ['title', 'body'] - valueField: 'title' + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'String', name: 'string', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean' } - { label: 'Text', name: 'text', widget: 'text' } diff --git a/dev-test/backends/test/index.html b/dev-test/backends/test/index.html index ce344716..0a68d4e3 100644 --- a/dev-test/backends/test/index.html +++ b/dev-test/backends/test/index.html @@ -172,9 +172,9 @@ render: function() { // 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 - // 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 - // 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". const { value, fieldsMetaData } = this.props; const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]); diff --git a/dev-test/config.yml b/dev-test/config.yml index 2dd82bca..04cf85e8 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -34,8 +34,8 @@ collections: # A list of collections the CMS should be able to edit label: 'Publish Date', name: 'date', widget: 'datetime', - dateFormat: 'YYYY-MM-DD', - timeFormat: 'HH:mm', + date_format: 'YYYY-MM-DD', + time_format: 'HH:mm', format: 'YYYY-MM-DD HH:mm', } - label: 'Cover Image' @@ -108,9 +108,9 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - displayFields: ['title', 'date'] - searchFields: ['title', 'body'] - valueField: 'title' + display_fields: ['title', 'date'] + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'Title', name: 'title', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true } - { label: 'Map', name: 'map', widget: 'map' } @@ -139,8 +139,8 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - searchFields: ['title', 'body'] - valueField: 'title' + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'String', name: 'string', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false } - { label: 'Text', name: 'text', widget: 'text' } @@ -187,8 +187,8 @@ collections: # A list of collections the CMS should be able to edit name: 'post' widget: 'relationKitchenSinkPost' collection: 'posts' - searchFields: ['title', 'body'] - valueField: 'title' + search_fields: ['title', 'body'] + value_field: 'title' - { label: 'String', name: 'string', widget: 'string' } - { label: 'Boolean', name: 'boolean', widget: 'boolean' } - { label: 'Text', name: 'text', widget: 'text' } diff --git a/dev-test/index.html b/dev-test/index.html index ce344716..0a68d4e3 100644 --- a/dev-test/index.html +++ b/dev-test/index.html @@ -172,9 +172,9 @@ render: function() { // 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 - // 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 - // 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". const { value, fieldsMetaData } = this.props; const post = fieldsMetaData && fieldsMetaData.getIn(['posts', value]); diff --git a/packages/netlify-cms-core/index.d.ts b/packages/netlify-cms-core/index.d.ts index 9c9af947..0c961d9f 100644 --- a/packages/netlify-cms-core/index.d.ts +++ b/packages/netlify-cms-core/index.d.ts @@ -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[]; } diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js index 7157b2d7..c04c05dc 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js @@ -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', () => { diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js index 64608267..b7cd7219 100644 --- a/packages/netlify-cms-core/src/actions/config.js +++ b/packages/netlify-cms-core/src/actions/config.js @@ -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()); diff --git a/packages/netlify-cms-core/src/components/Collection/__tests__/Collection.spec.js b/packages/netlify-cms-core/src/components/Collection/__tests__/Collection.spec.js index 340f630a..38f83a44 100644 --- a/packages/netlify-cms-core/src/components/Collection/__tests__/Collection.spec.js +++ b/packages/netlify-cms-core/src/components/Collection/__tests__/Collection.spec.js @@ -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, diff --git a/packages/netlify-cms-core/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap b/packages/netlify-cms-core/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap index 212461d3..50aa5d67 100644 --- a/packages/netlify-cms-core/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap +++ b/packages/netlify-cms-core/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap @@ -14,8 +14,8 @@ exports[`Collection should render connected component 1`] = ` class="emotion-2 emotion-3" > @@ -23,7 +23,7 @@ exports[`Collection should render connected component 1`] = ` class="emotion-0 emotion-1" > @@ -54,19 +54,19 @@ exports[`Collection should render with collection with create url 1`] = ` class="emotion-2 emotion-3" >
@@ -87,20 +87,20 @@ exports[`Collection should render with collection with create url and path 1`] = class="emotion-2 emotion-3" >
@@ -122,19 +122,19 @@ exports[`Collection should render with collection without create url 1`] = ` class="emotion-2 emotion-3" >
diff --git a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js index ee2a4fb6..f33cecad 100644 --- a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js @@ -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'], }, ], }, diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index b1269830..1ac33377 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -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. diff --git a/packages/netlify-cms-core/src/reducers/collections.ts b/packages/netlify-cms-core/src/reducers/collections.ts index 7b717e25..bea69fa9 100644 --- a/packages/netlify-cms-core/src/reducers/collections.ts +++ b/packages/netlify-cms-core/src/reducers/collections.ts @@ -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) { diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 3e05e8d0..d0502d4a 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -183,7 +183,7 @@ type CollectionObject = { slug?: string; label_singular?: string; label: string; - sortableFields: List; + sortable_fields: List; view_filters: List>; nested?: Nested; meta?: Meta; diff --git a/packages/netlify-cms-widget-date/src/DateControl.js b/packages/netlify-cms-widget-date/src/DateControl.js index 497ff837..28e9ad82 100644 --- a/packages/netlify-cms-widget-date/src/DateControl.js +++ b/packages/netlify-cms-widget-date/src/DateControl.js @@ -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; } diff --git a/packages/netlify-cms-widget-datetime/src/DateTimeControl.js b/packages/netlify-cms-widget-datetime/src/DateTimeControl.js index a06f83f9..dd0d15fa 100644 --- a/packages/netlify-cms-widget-datetime/src/DateTimeControl.js +++ b/packages/netlify-cms-widget-datetime/src/DateTimeControl.js @@ -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; } diff --git a/packages/netlify-cms-widget-datetime/src/schema.js b/packages/netlify-cms-widget-datetime/src/schema.js index 45bc1079..8e32e7ab 100644 --- a/packages/netlify-cms-widget-datetime/src/schema.js +++ b/packages/netlify-cms-widget-datetime/src/schema.js @@ -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' }, }, }; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index bfdb5663..003a309a 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -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} diff --git a/packages/netlify-cms-widget-markdown/src/schema.js b/packages/netlify-cms-widget-markdown/src/schema.js index 3342c358..86ffd377 100644 --- a/packages/netlify-cms-widget-markdown/src/schema.js +++ b/packages/netlify-cms-widget-markdown/src/schema.js @@ -22,6 +22,6 @@ export default { ], }, }, - editorComponents: { type: 'array', items: { type: 'string' } }, + editor_components: { type: 'array', items: { type: 'string' } }, }, }; diff --git a/packages/netlify-cms-widget-number/src/NumberControl.js b/packages/netlify-cms-widget-number/src/NumberControl.js index e6b314c4..35606d9e 100644 --- a/packages/netlify-cms-widget-number/src/NumberControl.js +++ b/packages/netlify-cms-widget-number/src/NumberControl.js @@ -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 ( { }); 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 }); diff --git a/packages/netlify-cms-widget-number/src/schema.js b/packages/netlify-cms-widget-number/src/schema.js index 899aef25..4f2f1600 100644 --- a/packages/netlify-cms-widget-number/src/schema.js +++ b/packages/netlify-cms-widget-number/src/schema.js @@ -1,7 +1,7 @@ export default { properties: { step: { type: 'number' }, - valueType: { type: 'string' }, + value_type: { type: 'string' }, min: { type: 'number' }, max: { type: 'number' }, }, diff --git a/packages/netlify-cms-widget-relation/src/RelationControl.js b/packages/netlify-cms-widget-relation/src/RelationControl.js index 8ba3e100..0985dbf9 100644 --- a/packages/netlify-cms-widget-relation/src/RelationControl.js +++ b/packages/netlify-cms-widget-relation/src/RelationControl.js @@ -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'); diff --git a/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js b/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js index 98f80ede..a21a925f 100644 --- a/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js +++ b/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js @@ -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' }); diff --git a/packages/netlify-cms-widget-relation/src/schema.js b/packages/netlify-cms-widget-relation/src/schema.js index c84dd013..bec450d5 100644 --- a/packages/netlify-cms-widget-relation/src/schema.js +++ b/packages/netlify-cms-widget-relation/src/schema.js @@ -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'], + }, + ], }; diff --git a/website/content/docs/configuration-options.md b/website/content/docs/configuration-options.md index b3f9c97f..68c5813c 100644 --- a/website/content/docs/configuration-options.md +++ b/website/content/docs/configuration-options.md @@ -209,7 +209,7 @@ The `collections` setting is the heart of your Netlify CMS configuration, as it * `fields` (required): see detailed description below * `editor`: 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 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}}" ``` -### `sortableFields` +### `sortable_fields` 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 # 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` diff --git a/website/content/docs/jekyll.md b/website/content/docs/jekyll.md index 2ee3f591..73a488c6 100644 --- a/website/content/docs/jekyll.md +++ b/website/content/docs/jekyll.md @@ -201,9 +201,9 @@ fields: name: 'author', widget: 'relation', collection: 'authors', - displayFields: [display_name], - searchFields: [display_name], - valueField: 'name', + display_fields: [display_name], + search_fields: [display_name], + value_field: 'name', } - { label: 'Body', name: 'body', widget: 'markdown' } ``` diff --git a/website/content/docs/widgets/date.md b/website/content/docs/widgets/date.md index c7110630..948ec64c 100644 --- a/website/content/docs/widgets/date.md +++ b/website/content/docs/widgets/date.md @@ -13,8 +13,8 @@ The date widget translates a date picker input to a date string. For saving date - **Options:** - `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) - - `dateFormat`: 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. + - `date_format`: optional; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format. + - `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:** ```yaml - label: 'Birthdate' diff --git a/website/content/docs/widgets/datetime.md b/website/content/docs/widgets/datetime.md index d14ffa57..5ab6c5eb 100644 --- a/website/content/docs/widgets/datetime.md +++ b/website/content/docs/widgets/datetime.md @@ -11,17 +11,17 @@ The datetime widget translates a datetime picker to a datetime string. - **Options:** - `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) - - `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. - - `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. - - `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. + - `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. + - `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. + - `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:** ```yaml - label: "Start time" name: "start" widget: "datetime" default: "" - dateFormat: "DD.MM.YYYY" # e.g. 24.12.2021 - timeFormat: "HH:mm" # e.g. 21:07 + date_format: "DD.MM.YYYY" # e.g. 24.12.2021 + time_format: "HH:mm" # e.g. 21:07 format: "LLL" - pickerUtc: false + picker_utc: false ``` diff --git a/website/content/docs/widgets/markdown.md b/website/content/docs/widgets/markdown.md index b822c556..be478bf2 100644 --- a/website/content/docs/widgets/markdown.md +++ b/website/content/docs/widgets/markdown.md @@ -14,7 +14,7 @@ _Please note:_ If you want to use your markdown editor to fill a markdown file c - `default`: accepts markdown content - `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`. - - `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:** ```yaml diff --git a/website/content/docs/widgets/number.md b/website/content/docs/widgets/number.md index 88ce191c..e0ea666e 100644 --- a/website/content/docs/widgets/number.md +++ b/website/content/docs/widgets/number.md @@ -7,10 +7,10 @@ The number widget uses an HTML number input, saving the value as a string, integ - **Name:** `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:** - `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 - `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 @@ -20,7 +20,7 @@ The number widget uses an HTML number input, saving the value as a string, integ name: "puppies" widget: "number" default: 2 - valueType: "int" + value_type: "int" min: 1 max: 101 step: 2 diff --git a/website/content/docs/widgets/relation.md b/website/content/docs/widgets/relation.md index ea3b3e2b..d7b536cb 100644 --- a/website/content/docs/widgets/relation.md +++ b/website/content/docs/widgets/relation.md @@ -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 - **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`). 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*. + - `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.*`). + - `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) - - `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 - `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): ```yaml @@ -24,9 +24,9 @@ The relation widget allows you to reference items from another collection. It pr name: "author" widget: "relation" collection: "authors" - searchFields: ["name.first", "twitterHandle"] - valueField: "name.first" - displayFields: ["twitterHandle", "followerCount"] + search_fields: ["name.first", "twitterHandle"] + value_field: "name.first" + 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. @@ -38,9 +38,9 @@ The generated UI input will search the authors collection by name and twitterHan name: "author" widget: "relation" collection: "authors" - searchFields: ['name.first'] - valueField: "{{slug}}" - displayFields: ["{{twitterHandle}} - {{followerCount}}"] + search_fields: ['name.first'] + value_field: "{{slug}}" + 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. @@ -53,9 +53,9 @@ The generated UI input will search the authors collection by name, and display e widget: "relation" collection: "relation_files" file: "cities" - searchFields: ["cities.*.name"] - displayFields: ["cities.*.name"] - valueField: "cities.*.id" + search_fields: ["cities.*.name"] + display_fields: ["cities.*.name"] + 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.