fix: deprecate inconsistent config param case (#4172)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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