feat: add widgets schema validation (#3841)
This commit is contained in:
parent
995739a85c
commit
2b46608f86
packages
netlify-cms-core/src
netlify-cms-widget-code/src
netlify-cms-widget-datetime/src
netlify-cms-widget-file/src
netlify-cms-widget-image/src
netlify-cms-widget-list/src
netlify-cms-widget-map/src
netlify-cms-widget-markdown/src
netlify-cms-widget-number/src
netlify-cms-widget-object/src
netlify-cms-widget-relation/src
netlify-cms-widget-select/src
website/content/docs
@ -1,6 +1,8 @@
|
|||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import { validateConfig } from '../configSchema';
|
import { validateConfig } from '../configSchema';
|
||||||
|
|
||||||
|
jest.mock('../../lib/registry');
|
||||||
|
|
||||||
describe('config', () => {
|
describe('config', () => {
|
||||||
/**
|
/**
|
||||||
* Suppress error logging to reduce noise during testing. Jest will still
|
* Suppress error logging to reduce noise during testing. Jest will still
|
||||||
@ -10,6 +12,9 @@ describe('config', () => {
|
|||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { getWidgets } = require('../../lib/registry');
|
||||||
|
getWidgets.mockImplementation(() => [{}]);
|
||||||
|
|
||||||
describe('validateConfig', () => {
|
describe('validateConfig', () => {
|
||||||
const validConfig = {
|
const validConfig = {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
@ -234,5 +239,82 @@ describe('config', () => {
|
|||||||
);
|
);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('nested validation', () => {
|
||||||
|
const { getWidgets } = require('../../lib/registry');
|
||||||
|
getWidgets.mockImplementation(() => [
|
||||||
|
{
|
||||||
|
name: 'relation',
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
searchFields: { type: 'array', items: { type: 'string' } },
|
||||||
|
displayFields: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('should throw if nested relation displayFields and searchFields are not arrays', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateConfig(
|
||||||
|
merge(validConfig, {
|
||||||
|
collections: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', label: 'title', widget: 'string' },
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
label: 'Object',
|
||||||
|
widget: 'object',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', label: 'title', widget: 'string' },
|
||||||
|
{
|
||||||
|
name: 'relation',
|
||||||
|
label: 'relation',
|
||||||
|
widget: 'relation',
|
||||||
|
displayFields: 'title',
|
||||||
|
searchFields: 'title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).toThrowError("'searchFields' should be array\n'displayFields' should be array");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if nested relation displayFields and searchFields are arrays', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateConfig(
|
||||||
|
merge(validConfig, {
|
||||||
|
collections: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', label: 'title', widget: 'string' },
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
label: 'Object',
|
||||||
|
widget: 'object',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', label: 'title', widget: 'string' },
|
||||||
|
{
|
||||||
|
name: 'relation',
|
||||||
|
label: 'relation',
|
||||||
|
widget: 'relation',
|
||||||
|
displayFields: ['title'],
|
||||||
|
searchFields: ['title'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,27 +1,39 @@
|
|||||||
import AJV from 'ajv';
|
import AJV from 'ajv';
|
||||||
import uniqueItemProperties from 'ajv-keywords/keywords/uniqueItemProperties';
|
import { select, uniqueItemProperties } from 'ajv-keywords/keywords';
|
||||||
import ajvErrors from 'ajv-errors';
|
import ajvErrors from 'ajv-errors';
|
||||||
import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats';
|
import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats';
|
||||||
|
import { getWidgets } from 'Lib/registry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config for fields in both file and folder collections.
|
* Config for fields in both file and folder collections.
|
||||||
*/
|
*/
|
||||||
const fieldsConfig = {
|
const fieldsConfig = () => ({
|
||||||
|
$id: 'fields',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
minItems: 1,
|
minItems: 1,
|
||||||
items: {
|
items: {
|
||||||
// ------- Each field: -------
|
// ------- Each field: -------
|
||||||
|
$id: 'field',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: 'string' },
|
name: { type: 'string' },
|
||||||
label: { type: 'string' },
|
label: { type: 'string' },
|
||||||
widget: { type: 'string' },
|
widget: { type: 'string' },
|
||||||
required: { type: 'boolean' },
|
required: { type: 'boolean' },
|
||||||
|
hint: { type: 'string' },
|
||||||
|
pattern: { type: 'array', minItems: 2, items: { type: 'string' } },
|
||||||
|
field: { $ref: 'field' },
|
||||||
|
fields: { $ref: 'fields' },
|
||||||
|
types: { $ref: 'fields' },
|
||||||
|
},
|
||||||
|
select: { $data: '0/widget' },
|
||||||
|
selectCases: {
|
||||||
|
...getWidgetSchemas(),
|
||||||
},
|
},
|
||||||
required: ['name'],
|
required: ['name'],
|
||||||
},
|
},
|
||||||
uniqueItemProperties: ['name'],
|
uniqueItemProperties: ['name'],
|
||||||
};
|
});
|
||||||
|
|
||||||
const viewFilters = {
|
const viewFilters = {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@ -133,7 +145,7 @@ const getConfigSchema = () => ({
|
|||||||
label_singular: { type: 'string' },
|
label_singular: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
file: { type: 'string' },
|
file: { type: 'string' },
|
||||||
fields: fieldsConfig,
|
fields: fieldsConfig(),
|
||||||
},
|
},
|
||||||
required: ['name', 'label', 'file', 'fields'],
|
required: ['name', 'label', 'file', 'fields'],
|
||||||
},
|
},
|
||||||
@ -163,7 +175,7 @@ const getConfigSchema = () => ({
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fields: fieldsConfig,
|
fields: fieldsConfig(),
|
||||||
sortableFields: {
|
sortableFields: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
@ -199,6 +211,11 @@ const getConfigSchema = () => ({
|
|||||||
anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
|
anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getWidgetSchemas() {
|
||||||
|
const schemas = getWidgets().map(widget => ({ [widget.name]: widget.schema }));
|
||||||
|
return Object.assign(...schemas);
|
||||||
|
}
|
||||||
|
|
||||||
class ConfigError extends Error {
|
class ConfigError extends Error {
|
||||||
constructor(errors, ...args) {
|
constructor(errors, ...args) {
|
||||||
const message = errors
|
const message = errors
|
||||||
@ -228,8 +245,9 @@ class ConfigError extends Error {
|
|||||||
* the config that is passed in.
|
* the config that is passed in.
|
||||||
*/
|
*/
|
||||||
export function validateConfig(config) {
|
export function validateConfig(config) {
|
||||||
const ajv = new AJV({ allErrors: true, jsonPointers: true });
|
const ajv = new AJV({ allErrors: true, jsonPointers: true, $data: true });
|
||||||
uniqueItemProperties(ajv);
|
uniqueItemProperties(ajv);
|
||||||
|
select(ajv);
|
||||||
ajvErrors(ajv);
|
ajvErrors(ajv);
|
||||||
|
|
||||||
const valid = ajv.validate(getConfigSchema(), config);
|
const valid = ajv.validate(getConfigSchema(), config);
|
||||||
|
@ -82,7 +82,7 @@ export function getPreviewTemplate(name) {
|
|||||||
/**
|
/**
|
||||||
* Editor Widgets
|
* Editor Widgets
|
||||||
*/
|
*/
|
||||||
export function registerWidget(name, control, preview) {
|
export function registerWidget(name, control, preview, schema = {}) {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
name.forEach(widget => {
|
name.forEach(widget => {
|
||||||
if (typeof widget !== 'object') {
|
if (typeof widget !== 'object') {
|
||||||
@ -95,12 +95,13 @@ export function registerWidget(name, control, preview) {
|
|||||||
// A registered widget control can be reused by a new widget, allowing
|
// A registered widget control can be reused by a new widget, allowing
|
||||||
// multiple copies with different previews.
|
// multiple copies with different previews.
|
||||||
const newControl = typeof control === 'string' ? registry.widgets[control].control : control;
|
const newControl = typeof control === 'string' ? registry.widgets[control].control : control;
|
||||||
registry.widgets[name] = { control: newControl, preview };
|
registry.widgets[name] = { control: newControl, preview, schema };
|
||||||
} else if (typeof name === 'object') {
|
} else if (typeof name === 'object') {
|
||||||
const {
|
const {
|
||||||
name: widgetName,
|
name: widgetName,
|
||||||
controlComponent: control,
|
controlComponent: control,
|
||||||
previewComponent: preview,
|
previewComponent: preview,
|
||||||
|
schema = {},
|
||||||
allowMapValue,
|
allowMapValue,
|
||||||
globalStyles,
|
globalStyles,
|
||||||
...options
|
...options
|
||||||
@ -114,7 +115,14 @@ export function registerWidget(name, control, preview) {
|
|||||||
if (!control) {
|
if (!control) {
|
||||||
throw Error(`Widget "${widgetName}" registered without \`controlComponent\`.`);
|
throw Error(`Widget "${widgetName}" registered without \`controlComponent\`.`);
|
||||||
}
|
}
|
||||||
registry.widgets[widgetName] = { control, preview, globalStyles, allowMapValue, ...options };
|
registry.widgets[widgetName] = {
|
||||||
|
control,
|
||||||
|
preview,
|
||||||
|
schema,
|
||||||
|
globalStyles,
|
||||||
|
allowMapValue,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
console.error('`registerWidget` failed, called with incorrect arguments.');
|
console.error('`registerWidget` failed, called with incorrect arguments.');
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './CodeControl';
|
import controlComponent from './CodeControl';
|
||||||
import previewComponent from './CodePreview';
|
import previewComponent from './CodePreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'code',
|
name: 'code',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
allowMapValue: true,
|
allowMapValue: true,
|
||||||
codeMirrorConfig: {},
|
codeMirrorConfig: {},
|
||||||
...opts,
|
...opts,
|
||||||
|
11
packages/netlify-cms-widget-code/src/schema.js
Normal file
11
packages/netlify-cms-widget-code/src/schema.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
default_language: { type: 'string' },
|
||||||
|
allow_language_selection: { type: 'boolean' },
|
||||||
|
output_code_only: { type: 'boolean' },
|
||||||
|
keys: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { code: { type: 'string' }, lang: { type: 'string' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './DateTimeControl';
|
import controlComponent from './DateTimeControl';
|
||||||
import previewComponent from './DateTimePreview';
|
import previewComponent from './DateTimePreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'datetime',
|
name: 'datetime',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
8
packages/netlify-cms-widget-datetime/src/schema.js
Normal file
8
packages/netlify-cms-widget-datetime/src/schema.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
format: { type: 'string' },
|
||||||
|
dateFormat: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
|
||||||
|
timeFormat: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
|
||||||
|
pickerUtc: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,11 +1,13 @@
|
|||||||
import withFileControl from './withFileControl';
|
import withFileControl from './withFileControl';
|
||||||
import previewComponent from './FilePreview';
|
import previewComponent from './FilePreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const controlComponent = withFileControl();
|
const controlComponent = withFileControl();
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'file',
|
name: 'file',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
packages/netlify-cms-widget-file/src/schema.js
Normal file
5
packages/netlify-cms-widget-file/src/schema.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
allow_multiple: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,11 +1,13 @@
|
|||||||
import NetlifyCmsWidgetFile from 'netlify-cms-widget-file';
|
import NetlifyCmsWidgetFile from 'netlify-cms-widget-file';
|
||||||
import previewComponent from './ImagePreview';
|
import previewComponent from './ImagePreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const controlComponent = NetlifyCmsWidgetFile.withFileControl({ forImage: true });
|
const controlComponent = NetlifyCmsWidgetFile.withFileControl({ forImage: true });
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'image',
|
name: 'image',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
packages/netlify-cms-widget-image/src/schema.js
Normal file
5
packages/netlify-cms-widget-image/src/schema.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
allow_multiple: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,11 +1,13 @@
|
|||||||
import controlComponent from './ListControl';
|
import controlComponent from './ListControl';
|
||||||
import NetlifyCmsWidgetObject from 'netlify-cms-widget-object';
|
import NetlifyCmsWidgetObject from 'netlify-cms-widget-object';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const previewComponent = NetlifyCmsWidgetObject.previewComponent;
|
const previewComponent = NetlifyCmsWidgetObject.previewComponent;
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'list',
|
name: 'list',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
9
packages/netlify-cms-widget-list/src/schema.js
Normal file
9
packages/netlify-cms-widget-list/src/schema.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
allow_add: { type: 'boolean' },
|
||||||
|
collapsed: { type: 'boolean' },
|
||||||
|
summary: { type: 'string' },
|
||||||
|
minimize_collapsed: { type: 'boolean' },
|
||||||
|
label_singular: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,11 +1,13 @@
|
|||||||
import withMapControl from './withMapControl';
|
import withMapControl from './withMapControl';
|
||||||
import previewComponent from './MapPreview';
|
import previewComponent from './MapPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const controlComponent = withMapControl();
|
const controlComponent = withMapControl();
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'map',
|
name: 'map',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
6
packages/netlify-cms-widget-map/src/schema.js
Normal file
6
packages/netlify-cms-widget-map/src/schema.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
decimals: { type: 'integer' },
|
||||||
|
type: { type: 'string', enum: ['Point', 'LineString', 'Polygon'] },
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './MarkdownControl';
|
import controlComponent from './MarkdownControl';
|
||||||
import previewComponent from './MarkdownPreview';
|
import previewComponent from './MarkdownPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'markdown',
|
name: 'markdown',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
27
packages/netlify-cms-widget-markdown/src/schema.js
Normal file
27
packages/netlify-cms-widget-markdown/src/schema.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
minimal: { type: 'boolean' },
|
||||||
|
buttons: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: [
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'code',
|
||||||
|
'link',
|
||||||
|
'heading-one',
|
||||||
|
'heading-two',
|
||||||
|
'heading-three',
|
||||||
|
'heading-four',
|
||||||
|
'heading-five',
|
||||||
|
'heading-six',
|
||||||
|
'quote',
|
||||||
|
'bulleted-list',
|
||||||
|
'numbered-list',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editorComponents: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './NumberControl';
|
import controlComponent from './NumberControl';
|
||||||
import previewComponent from './NumberPreview';
|
import previewComponent from './NumberPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'number',
|
name: 'number',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
8
packages/netlify-cms-widget-number/src/schema.js
Normal file
8
packages/netlify-cms-widget-number/src/schema.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
step: { type: 'integer' },
|
||||||
|
valueType: { type: 'string' },
|
||||||
|
min: { type: 'integer' },
|
||||||
|
max: { type: 'integer' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './ObjectControl';
|
import controlComponent from './ObjectControl';
|
||||||
import previewComponent from './ObjectPreview';
|
import previewComponent from './ObjectPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'object',
|
name: 'object',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
packages/netlify-cms-widget-object/src/schema.js
Normal file
5
packages/netlify-cms-widget-object/src/schema.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
collapsed: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './RelationControl';
|
import controlComponent from './RelationControl';
|
||||||
import previewComponent from './RelationPreview';
|
import previewComponent from './RelationPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'relation',
|
name: 'relation',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
12
packages/netlify-cms-widget-relation/src/schema.js
Normal file
12
packages/netlify-cms-widget-relation/src/schema.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
collection: { type: 'string' },
|
||||||
|
valueField: { type: 'string' },
|
||||||
|
searchFields: { type: 'array', minItems: 1, items: { type: 'string' } },
|
||||||
|
file: { type: 'string' },
|
||||||
|
multiple: { type: 'boolean' },
|
||||||
|
displayFields: { type: 'array', minItems: 1, items: { type: 'string' } },
|
||||||
|
optionsLength: { type: 'integer' },
|
||||||
|
},
|
||||||
|
required: ['collection', 'valueField', 'searchFields'],
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import controlComponent from './SelectControl';
|
import controlComponent from './SelectControl';
|
||||||
import previewComponent from './SelectPreview';
|
import previewComponent from './SelectPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
const Widget = (opts = {}) => ({
|
const Widget = (opts = {}) => ({
|
||||||
name: 'select',
|
name: 'select',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
schema,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
24
packages/netlify-cms-widget-select/src/schema.js
Normal file
24
packages/netlify-cms-widget-select/src/schema.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
multiple: { type: 'boolean' },
|
||||||
|
min: { type: 'integer' },
|
||||||
|
max: { type: 'integer' },
|
||||||
|
options: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
label: { type: 'string' },
|
||||||
|
value: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['label', 'value'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['options'],
|
||||||
|
};
|
@ -23,11 +23,11 @@ Register a custom widget.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
// Using global window object
|
// Using global window object
|
||||||
CMS.registerWidget(name, control, [preview]);
|
CMS.registerWidget(name, control, [preview], [schema]);
|
||||||
|
|
||||||
// Using npm module import
|
// Using npm module import
|
||||||
import CMS from 'netlify-cms';
|
import CMS from 'netlify-cms';
|
||||||
CMS.registerWidget(name, control, [preview]);
|
CMS.registerWidget(name, control, [preview], [schema]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Params:**
|
**Params:**
|
||||||
@ -35,29 +35,34 @@ CMS.registerWidget(name, control, [preview]);
|
|||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
| ----------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `name` | `string` | Widget name, allows this widget to be used via the field `widget` property in config |
|
| `name` | `string` | Widget name, allows this widget to be used via the field `widget` property in config |
|
||||||
| `control` | `React.Component` or `string`| <ul><li>React component that renders the control, receives the following props: <ul><li>**value:** Current field value</li><li>**onChange:** Callback function to update the field value</li></ul></li><li>Name of a registered widget whose control should be used (includes built in widgets).</li></ul> |
|
| `control` | `React.Component` or `string`| <ul><li>React component that renders the control, receives the following props: <ul><li>**value:** Current field value</li><li>**field:** Immutable map of current field configuration</li><li>**forID:** Unique identifier for the field</li><li>**classNameWrapper:** class name to apply CMS styling to the field</li><li>**onChange:** Callback function to update the field value</li></ul></li><li>Name of a registered widget whose control should be used (includes built in widgets).</li></ul> |
|
||||||
| [`preview`] | `React.Component`, optional | Renders the widget preview, receives the following props: <ul><li>**value:** Current preview value</li><li>**field:** Immutable map of current field configuration</li><li>**metadata:** Immutable map of any available metadata for the current field</li><li>**getAsset:** Function for retrieving an asset url for image/file fields</li><li>**entry:** Immutable Map of all entry data</li><li>**fieldsMetaData:** Immutable map of metadata from all fields.</li></ul> |
|
| [`preview`] | `React.Component`, optional | Renders the widget preview, receives the following props: <ul><li>**value:** Current preview value</li><li>**field:** Immutable map of current field configuration</li><li>**metadata:** Immutable map of any available metadata for the current field</li><li>**getAsset:** Function for retrieving an asset url for image/file fields</li><li>**entry:** Immutable Map of all entry data</li><li>**fieldsMetaData:** Immutable map of metadata from all fields.</li></ul> |
|
||||||
|
| [`schema`] | `JSON Schema object`, optional | Enforces a schema for the widget's field configuration
|
||||||
* **field:** The field type that this widget will be used for.
|
|
||||||
* **control:** A React component that renders the editing interface for this field. Two props will be passed:
|
|
||||||
* **value:** The current value for this field.
|
|
||||||
* **onChange:** Callback function to update the field value.
|
|
||||||
* **preview (optional):** A React component that renders the preview of how the content will look. A `value` prop will be passed to this component.
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
`admin/index.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var CategoriesControl = createClass({
|
var CategoriesControl = createClass({
|
||||||
handleChange: function(e) {
|
handleChange: function(e) {
|
||||||
this.props.onChange(e.target.value.split(',').map((e) => e.trim()));
|
const separator = this.props.field.get('separator', ', ')
|
||||||
|
this.props.onChange(e.target.value.split(separator).map((e) => e.trim()));
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const separator = this.props.field.get('separator', ', ');
|
||||||
var value = this.props.value;
|
var value = this.props.value;
|
||||||
return h('input', { type: 'text', value: value ? value.join(', ') : '', onChange: this.handleChange });
|
return h('input', {
|
||||||
}
|
id: this.props.forID,
|
||||||
|
className: this.props.classNameWrapper,
|
||||||
|
type: 'text',
|
||||||
|
value: value ? value.join(separator) : '',
|
||||||
|
onChange: this.handleChange,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var CategoriesPreview = createClass({
|
var CategoriesPreview = createClass({
|
||||||
@ -70,10 +75,33 @@ var CategoriesPreview = createClass({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
CMS.registerWidget('categories', CategoriesControl, CategoriesPreview);
|
var schema = {
|
||||||
|
properties: {
|
||||||
|
separator: { type: 'string' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, schema);
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`admin/config.yml`
|
||||||
|
|
||||||
|
```yml
|
||||||
|
collections:
|
||||||
|
- name: posts
|
||||||
|
label: Posts
|
||||||
|
folder: content/posts
|
||||||
|
fields:
|
||||||
|
- name: title
|
||||||
|
label: Title
|
||||||
|
widget: string
|
||||||
|
- name: categories
|
||||||
|
label: Categories
|
||||||
|
widget: categories
|
||||||
|
separator: __
|
||||||
|
```
|
||||||
|
|
||||||
## `registerEditorComponent`
|
## `registerEditorComponent`
|
||||||
|
|
||||||
Register a block level component for the Markdown editor:
|
Register a block level component for the Markdown editor:
|
||||||
|
@ -10,10 +10,11 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
|||||||
- **Data type:** list of widget values
|
- **Data type:** list of widget values
|
||||||
- **Options:**
|
- **Options:**
|
||||||
- `default`: if `fields` is specified, declare defaults on the child widgets; if not, you may specify a list of strings to populate the text field
|
- `default`: if `fields` is specified, declare defaults on the child widgets; if not, you may specify a list of strings to populate the text field
|
||||||
- `allow_add`: if added and labeled `false`, button to add additional widgets disappears
|
- `allow_add`: if added and set to `false`, hides the button to add additional items
|
||||||
- `collapsed`: if added and labeled `false`, the list widget's content does not collapse by default
|
- `collapsed`: if added and set to `false`, the list widget's content does not collapse by default
|
||||||
- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary)
|
- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary)
|
||||||
- `minimize_collapsed`: if added and labeled `true`, the list widget's content will be completely hidden instead of only collapsed if the list widget itself is collapsed
|
- `minimize_collapsed`: if added and set to `true`, the list widget's content will be completely hidden instead of only collapsed if the list widget itself is collapsed
|
||||||
|
- `label_singular`: singular label to show as a part of the add button
|
||||||
- `field`: a single widget field to be repeated
|
- `field`: a single widget field to be repeated
|
||||||
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
|
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
|
||||||
- **Example** (`field`/`fields` not specified):
|
- **Example** (`field`/`fields` not specified):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user