planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

View File

@@ -0,0 +1,118 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
const ValidationErrorTypes = {
PRESENCE: 'PRESENCE',
PATTERN: 'PATTERN',
RANGE: 'RANGE',
CUSTOM: 'CUSTOM',
};
export function validateMinMax(value, min, max, field, t) {
let error;
switch (true) {
case value !== '' && min !== false && max !== false && (value < min || value > max):
error = {
type: ValidationErrorTypes.RANGE,
message: t('editor.editorControlPane.widget.range', {
fieldLabel: field.get('label', field.get('name')),
minValue: min,
maxValue: max,
}),
};
break;
case value !== '' && min !== false && value < min:
error = {
type: ValidationErrorTypes.RANGE,
message: t('editor.editorControlPane.widget.min', {
fieldLabel: field.get('label', field.get('name')),
minValue: min,
}),
};
break;
case value !== '' && max !== false && value > max:
error = {
type: ValidationErrorTypes.RANGE,
message: t('editor.editorControlPane.widget.max', {
fieldLabel: field.get('label', field.get('name')),
maxValue: max,
}),
};
break;
default:
error = null;
break;
}
return error;
}
export default class NumberControl extends React.Component {
static propTypes = {
field: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
classNameWrapper: PropTypes.string.isRequired,
setActiveStyle: PropTypes.func.isRequired,
setInactiveStyle: PropTypes.func.isRequired,
value: PropTypes.node,
forID: PropTypes.string,
valueType: PropTypes.string,
step: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
t: PropTypes.func.isRequired,
};
static defaultProps = {
value: '',
};
handleChange = e => {
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);
if (!isNaN(value)) {
onChange(value);
} else {
onChange('');
}
};
isValid = () => {
const { field, value, t } = this.props;
const hasPattern = !!field.get('pattern', false);
const min = field.get('min', false);
const max = field.get('max', false);
// Pattern overrides min/max logic always:
if (hasPattern) {
return true;
}
const error = validateMinMax(value, min, max, field, t);
return error ? { error } : true;
};
render() {
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('value_type') === 'int' ? 1 : '');
return (
<input
type="number"
id={forID}
className={classNameWrapper}
onFocus={setActiveStyle}
onBlur={setInactiveStyle}
value={value || (value === 0 ? value : '')}
step={step}
min={min}
max={max}
onChange={this.handleChange}
/>
);
}
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { WidgetPreviewContainer } from 'decap-cms-ui-default';
function NumberPreview({ value }) {
return <WidgetPreviewContainer>{value}</WidgetPreviewContainer>;
}
NumberPreview.propTypes = {
value: PropTypes.node,
};
export default NumberPreview;

View File

@@ -0,0 +1,214 @@
import React from 'react';
import { fromJS } from 'immutable';
import { render, fireEvent } from '@testing-library/react';
import { DecapCmsWidgetNumber } from '../';
import { validateMinMax } from '../NumberControl';
const NumberControl = DecapCmsWidgetNumber.controlComponent;
const fieldSettings = {
min: -20,
max: 20,
step: 1,
value_type: 'int',
};
class NumberController extends React.Component {
state = {
value: this.props.defaultValue,
};
handleOnChange = jest.fn(value => {
this.setState({ value });
});
componentDidUpdate() {
this.props.onStateChange(this.state);
}
render() {
return this.props.children({
value: this.state.value,
handleOnChange: this.handleOnChange,
});
}
}
function setup({ field, defaultValue }) {
let renderArgs;
const stateChangeSpy = jest.fn();
const setActiveSpy = jest.fn();
const setInactiveSpy = jest.fn();
const helpers = render(
<NumberController defaultValue={defaultValue} onStateChange={stateChangeSpy}>
{({ value, handleOnChange }) => {
renderArgs = { value, onChangeSpy: handleOnChange };
return (
<NumberControl
field={field}
value={value}
onChange={handleOnChange}
forID="test-number"
classNameWrapper=""
setActiveStyle={setActiveSpy}
setInactiveStyle={setInactiveSpy}
t={jest.fn()}
/>
);
}}
</NumberController>,
);
const input = helpers.container.querySelector('input');
return {
...helpers,
...renderArgs,
stateChangeSpy,
setActiveSpy,
setInactiveSpy,
input,
};
}
describe('Number widget', () => {
it('should call onChange when input changes', () => {
const field = fromJS(fieldSettings);
const testValue = Math.floor(Math.random() * (20 - -20 + 1)) + -20;
const { input, onChangeSpy } = setup({ field });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(testValue);
});
it('should call onChange with empty string when no value is set', () => {
const field = fromJS(fieldSettings);
const { input, onChangeSpy } = setup({ field, defaultValue: 20 });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '' } });
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith('');
});
it('should call onChange with empty string when a non numeric value is set', () => {
const field = fromJS(fieldSettings);
const { input, onChangeSpy } = setup({ field, defaultValue: 20 });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: 'invalid' } });
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith('');
});
it('should parse float numbers as integers', () => {
const field = fromJS(fieldSettings);
const testValue = (Math.random() * (20 - -20 + 1) + -20).toFixed(2);
const { input, onChangeSpy } = setup({ field });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(parseInt(testValue, 10));
});
it('should parse float numbers as float', () => {
const field = fromJS({ ...fieldSettings, value_type: 'float' });
const testValue = (Math.random() * (20 - -20 + 1) + -20).toFixed(2);
const { input, onChangeSpy } = setup({ field });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(parseFloat(testValue));
});
it('should allow 0 as a value', () => {
const field = fromJS(fieldSettings);
const testValue = 0;
const { input } = setup({ field });
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });
expect(input.value).toBe('0');
});
describe('validateMinMax', () => {
const field = { get: jest.fn() };
field.get.mockReturnValue('label');
const t = jest.fn();
t.mockImplementation((_, params) => params);
beforeEach(() => {
jest.clearAllMocks();
});
it('should return error when min max are defined and value is out of range', () => {
const error = validateMinMax(5, 0, 1, field, t);
const expectedMessage = {
fieldLabel: 'label',
minValue: 0,
maxValue: 1,
};
expect(error).not.toBeNull();
expect(error).toEqual({
type: 'RANGE',
message: expectedMessage,
});
expect(t).toHaveBeenCalledTimes(1);
expect(t).toHaveBeenCalledWith('editor.editorControlPane.widget.range', expectedMessage);
});
it('should return error when min is defined and value is out of range', () => {
const error = validateMinMax(5, 6, false, field, t);
const expectedMessage = {
fieldLabel: 'label',
minValue: 6,
};
expect(error).not.toBeNull();
expect(error).toEqual({
type: 'RANGE',
message: expectedMessage,
});
expect(t).toHaveBeenCalledTimes(1);
expect(t).toHaveBeenCalledWith('editor.editorControlPane.widget.min', expectedMessage);
});
it('should return error when max is defined and value is out of range', () => {
const error = validateMinMax(5, false, 3, field, t);
const expectedMessage = {
fieldLabel: 'label',
maxValue: 3,
};
expect(error).not.toBeNull();
expect(error).toEqual({
type: 'RANGE',
message: expectedMessage,
});
expect(t).toHaveBeenCalledTimes(1);
expect(t).toHaveBeenCalledWith('editor.editorControlPane.widget.max', expectedMessage);
});
it('should not return error when min max are defined and value is empty', () => {
const error = validateMinMax('', 0, 1, field, t);
expect(error).toBeNull();
});
it('should not return error when min max are defined and value is in range', () => {
const error = validateMinMax(0, -1, 1, field, t);
expect(error).toBeNull();
});
});
});

16
node_modules/decap-cms-widget-number/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
import controlComponent from './NumberControl';
import previewComponent from './NumberPreview';
import schema from './schema';
function Widget(opts = {}) {
return {
name: 'number',
controlComponent,
previewComponent,
schema,
...opts,
};
}
export const DecapCmsWidgetNumber = { Widget, controlComponent, previewComponent };
export default DecapCmsWidgetNumber;

8
node_modules/decap-cms-widget-number/src/schema.js generated vendored Normal file
View File

@@ -0,0 +1,8 @@
export default {
properties: {
step: { type: 'number' },
value_type: { type: 'string' },
min: { type: 'number' },
max: { type: 'number' },
},
};