feat(widget-list): add min max configuration (#4394)
This commit is contained in:
parent
1bdd858b31
commit
5fdfe40dd2
@ -1,6 +1,8 @@
|
|||||||
import * as stringTemplate from './stringTemplate';
|
import * as stringTemplate from './stringTemplate';
|
||||||
|
import * as validations from './validations';
|
||||||
|
|
||||||
export const NetlifyCmsLibWidgets = {
|
export const NetlifyCmsLibWidgets = {
|
||||||
stringTemplate,
|
stringTemplate,
|
||||||
|
validations,
|
||||||
};
|
};
|
||||||
export { stringTemplate };
|
export { stringTemplate, validations };
|
||||||
|
28
packages/netlify-cms-lib-widgets/src/validations.ts
Normal file
28
packages/netlify-cms-lib-widgets/src/validations.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { isNumber } from 'lodash';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
export const validateMinMax = (
|
||||||
|
t: (key: string, options: unknown) => string,
|
||||||
|
fieldLabel: string,
|
||||||
|
value?: List<unknown>,
|
||||||
|
min?: number,
|
||||||
|
max?: number,
|
||||||
|
) => {
|
||||||
|
const minMaxError = (messageKey: string) => ({
|
||||||
|
type: 'RANGE',
|
||||||
|
message: t(`editor.editorControlPane.widget.${messageKey}`, {
|
||||||
|
fieldLabel,
|
||||||
|
minCount: min,
|
||||||
|
maxCount: max,
|
||||||
|
count: min,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if ([min, max, value?.size].every(isNumber) && (value!.size < min! || value!.size > max!)) {
|
||||||
|
return minMaxError(min === max ? 'rangeCountExact' : 'rangeCount');
|
||||||
|
} else if (isNumber(min) && min > 0 && value?.size && value.size < min) {
|
||||||
|
return minMaxError('rangeMin');
|
||||||
|
} else if (isNumber(max) && value?.size && value.size > max) {
|
||||||
|
return minMaxError('rangeMax');
|
||||||
|
}
|
||||||
|
};
|
@ -15,7 +15,7 @@ import {
|
|||||||
getErrorMessageForTypedFieldAndValue,
|
getErrorMessageForTypedFieldAndValue,
|
||||||
} from './typedListHelpers';
|
} from './typedListHelpers';
|
||||||
import { ListItemTopBar, ObjectWidgetTopBar, colors, lengths } from 'netlify-cms-ui-default';
|
import { ListItemTopBar, ObjectWidgetTopBar, colors, lengths } from 'netlify-cms-ui-default';
|
||||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
import { stringTemplate, validations } from 'netlify-cms-lib-widgets';
|
||||||
|
|
||||||
function valueToString(value) {
|
function valueToString(value) {
|
||||||
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
|
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
|
||||||
@ -261,6 +261,28 @@ export default class ListControl extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
this.props.validate();
|
this.props.validate();
|
||||||
}
|
}
|
||||||
|
this.props.onValidateObject(this.props.forID, this.validateSize());
|
||||||
|
};
|
||||||
|
|
||||||
|
validateSize = () => {
|
||||||
|
const { field, value, t } = this.props;
|
||||||
|
const min = field.get('min');
|
||||||
|
const max = field.get('max');
|
||||||
|
const required = field.get('required', true);
|
||||||
|
|
||||||
|
if (!required && !value?.size) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = validations.validateMinMax(
|
||||||
|
t,
|
||||||
|
field.get('label', field.get('name')),
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
);
|
||||||
|
|
||||||
|
return error ? [error] : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import { fromJS } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
import ListControl from '../ListControl';
|
import ListControl from '../ListControl';
|
||||||
|
|
||||||
@ -53,6 +53,7 @@ describe('ListControl', () => {
|
|||||||
path: 'posts/index.md',
|
path: 'posts/index.md',
|
||||||
}),
|
}),
|
||||||
forID: 'forID',
|
forID: 'forID',
|
||||||
|
t: key => key,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -629,4 +630,158 @@ describe('ListControl', () => {
|
|||||||
mock.mockRestore();
|
mock.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should give validation error if below min elements', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: true,
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([{ string: 'item 1' }]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||||
|
{
|
||||||
|
message: 'editor.editorControlPane.widget.rangeCount',
|
||||||
|
type: 'RANGE',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give min validation error if below min elements', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: true,
|
||||||
|
min: 2,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([{ string: 'item 1' }]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||||
|
{
|
||||||
|
message: 'editor.editorControlPane.widget.rangeMin',
|
||||||
|
type: 'RANGE',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give validation error if above max elements', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: true,
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([
|
||||||
|
{ string: 'item 1' },
|
||||||
|
{ string: 'item 2' },
|
||||||
|
{ string: 'item 3' },
|
||||||
|
{ string: 'item 4' },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||||
|
{
|
||||||
|
message: 'editor.editorControlPane.widget.rangeCount',
|
||||||
|
type: 'RANGE',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give max validation error if above max elements', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: true,
|
||||||
|
max: 3,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([
|
||||||
|
{ string: 'item 1' },
|
||||||
|
{ string: 'item 2' },
|
||||||
|
{ string: 'item 3' },
|
||||||
|
{ string: 'item 4' },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||||
|
{
|
||||||
|
message: 'editor.editorControlPane.widget.rangeMax',
|
||||||
|
type: 'RANGE',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give no validation error if between min and max elements', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: true,
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([{ string: 'item 1' }, { string: 'item 2' }, { string: 'item 3' }]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give no validation error if no elements and optional', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
required: false,
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const listControl = new ListControl({
|
||||||
|
...props,
|
||||||
|
field,
|
||||||
|
value: fromJS([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
listControl.validate();
|
||||||
|
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,5 +6,7 @@ export default {
|
|||||||
minimize_collapsed: { type: 'boolean' },
|
minimize_collapsed: { type: 'boolean' },
|
||||||
label_singular: { type: 'string' },
|
label_singular: { type: 'string' },
|
||||||
i18n: { type: 'boolean' },
|
i18n: { type: 'boolean' },
|
||||||
|
min: { type: 'number' },
|
||||||
|
max: { type: 'number' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"immutable": "^3.7.6",
|
"immutable": "^3.7.6",
|
||||||
|
"netlify-cms-lib-widgets": "^1.0.0",
|
||||||
"netlify-cms-ui-default": "^2.6.0",
|
"netlify-cms-ui-default": "^2.6.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.8.4",
|
"react": "^16.8.4",
|
||||||
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { Map, List, fromJS } from 'immutable';
|
import { Map, List, fromJS } from 'immutable';
|
||||||
import { find, isNumber } from 'lodash';
|
import { find } from 'lodash';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import { reactSelectStyles } from 'netlify-cms-ui-default';
|
import { reactSelectStyles } from 'netlify-cms-ui-default';
|
||||||
|
import { validations } from 'netlify-cms-lib-widgets';
|
||||||
|
|
||||||
function optionToString(option) {
|
function optionToString(option) {
|
||||||
return option && option.value ? option.value : null;
|
return option && option.value ? option.value : null;
|
||||||
@ -59,28 +60,20 @@ export default class SelectControl extends React.Component {
|
|||||||
const { field, value, t } = this.props;
|
const { field, value, t } = this.props;
|
||||||
const min = field.get('min');
|
const min = field.get('min');
|
||||||
const max = field.get('max');
|
const max = field.get('max');
|
||||||
const minMaxError = messageKey => ({
|
|
||||||
error: {
|
|
||||||
message: t(`editor.editorControlPane.widget.${messageKey}`, {
|
|
||||||
fieldLabel: field.get('label', field.get('name')),
|
|
||||||
minCount: min,
|
|
||||||
maxCount: max,
|
|
||||||
count: min,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!field.get('multiple')) {
|
if (!field.get('multiple')) {
|
||||||
return { error: false };
|
return { error: false };
|
||||||
}
|
}
|
||||||
if ([min, max].every(isNumber) && value?.size && (value.size < min || value.size > max)) {
|
|
||||||
return minMaxError(min === max ? 'rangeCountExact' : 'rangeCount');
|
const error = validations.validateMinMax(
|
||||||
} else if (isNumber(min) && min > 0 && value?.size && value.size < min) {
|
t,
|
||||||
return minMaxError('rangeMin');
|
field.get('label', field.get('name')),
|
||||||
} else if (isNumber(max) && value?.size && value.size > max) {
|
value,
|
||||||
return minMaxError('rangeMax');
|
min,
|
||||||
}
|
max,
|
||||||
return { error: false };
|
);
|
||||||
|
|
||||||
|
return error ? { error } : { error: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = selectedOption => {
|
handleChange = selectedOption => {
|
||||||
|
@ -17,6 +17,8 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
|||||||
* `label_singular`: the text to show on the add button
|
* `label_singular`: the text to show on 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
|
||||||
|
* `max`: maximum number of items in the list
|
||||||
|
* `min`: minimum number of items in the list
|
||||||
* **Example** (`field`/`fields` not specified):
|
* **Example** (`field`/`fields` not specified):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -81,3 +83,13 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
|||||||
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
|
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
|
||||||
- {label: Author, name: author, widget: string }
|
- {label: Author, name: author, widget: string }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* **Example** (with `max` & `min`):
|
||||||
|
```yaml
|
||||||
|
- label: "Tags"
|
||||||
|
name: "tags"
|
||||||
|
widget: "list"
|
||||||
|
max: 3
|
||||||
|
min: 1
|
||||||
|
default: ["news"]
|
||||||
|
```
|
Loading…
x
Reference in New Issue
Block a user