Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: performance improvements on input widgets #6565

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4321539
fix: fixes #6563 - performance improvements to pages with lists
geotrev Sep 28, 2022
c71d556
test: updates list widget tests to check null collapse section
geotrev Sep 30, 2022
24f92c1
refactor: remove unneeded display styles from list control's nested o…
geotrev Sep 30, 2022
3dd45b3
refactor: remove Object widget's unneeded display styles
geotrev Sep 30, 2022
7cfea01
refactor: debounce input values when updating redux store / document …
geotrev Oct 10, 2022
7a43b8c
test: fixes tests on Number widget
geotrev Oct 10, 2022
93050ec
test: adds tests for string widget
geotrev Oct 10, 2022
d90cf36
test: adds tests for text widget
geotrev Oct 10, 2022
f2a927d
test: adds tests for color widget
geotrev Oct 10, 2022
44f3d99
test: adds additional test cases to text and string widgets
geotrev Oct 10, 2022
dbd7cbf
Merge remote-tracking branch 'origin' into HEAD
martinjagodic Oct 18, 2023
11fe0a3
fix: move test specs to decap folders
martinjagodic Oct 18, 2023
798921d
fix: rename DecapCmsWidget
martinjagodic Oct 18, 2023
d95a2cf
Merge branch 'master' into 3415-list-lag-example
geotrev Jan 1, 2024
fa24144
fix: update refactored datetime with debounced value
geotrev Jan 1, 2024
be65bb5
test(): update some tests to handle field debounce
demshy Jan 22, 2024
f468127
test: use flushClockAndSave in i18n tests
demshy Jan 23, 2024
4f48602
Merge branch 'master' into 3415-list-lag-example
geotrev May 8, 2024
aac265f
fix(datetime): store correct datetime input value in state
geotrev May 8, 2024
4f60329
fix: revert list and object widget changes
geotrev May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/common/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const updateTranslation = () => {

enterTranslation('de de');
});
cy.contains('button', 'Save').click();
flushClockAndSave();
};

export const assertTranslation = () => {
Expand Down
8 changes: 4 additions & 4 deletions cypress/e2e/editorial_workflow_spec_test_backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
publishAndDuplicateEntryInEditor,
assertNotification,
assertFieldValidationError,
advanceClock,
flushClockAndSave,
} from '../utils/steps';
import { workflowStatus, editorStatus, publishTypes, notifications } from '../utils/constants';

Expand Down Expand Up @@ -253,10 +255,8 @@ describe('Test Backend Editorial Workflow', () => {
cy.get('[id^="path-field"]').should('have.value', 'directory/sub-directory');
cy.get('[id^="path-field"]').type('/new-path');
cy.get('[id^="title-field"]').type('New Path Title');
cy.clock().then(clock => {
clock.tick(150);
});
cy.contains('button', 'Save').click();
cy.clock().then(clock => { advanceClock(clock); });
flushClockAndSave();
assertNotification(notifications.saved);
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
Expand Down
19 changes: 10 additions & 9 deletions cypress/utils/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,12 @@ function validateObjectFields({ limit, author }) {
cy.get('a[href^="#/collections/settings"]').click();
cy.get('a[href^="#/collections/settings/entries/general"]').click();
cy.get('input[type=number]').type(limit);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertNotification(notifications.error.missingField);
assertFieldErrorStatus('Default Author', colorError);
cy.contains('label', 'Default Author').click();
cy.focused().type(author);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertNotification(notifications.saved);
assertFieldErrorStatus('Default Author', colorNormal);
}
Expand All @@ -464,28 +464,28 @@ function validateNestedObjectFields({ limit, author }) {
cy.get('a[href^="#/collections/settings/entries/general"]').click();
cy.contains('label', 'Default Author').click();
cy.focused().type(author);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertNotification(notifications.error.missingField);
cy.get('input[type=number]').type(limit + 1);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertFieldValidationError(notifications.validation.range);
cy.get('input[type=number]')
.clear()
.type(-1);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertFieldValidationError(notifications.validation.range);
cy.get('input[type=number]')
.clear()
.type(limit);
cy.contains('button', 'Save').click();
flushClockAndSave();
assertNotification(notifications.saved);
}

function validateListFields({ name, description }) {
cy.get('a[href^="#/collections/settings"]').click();
cy.get('a[href^="#/collections/settings/entries/authors"]').click();
cy.contains('button', 'Add').click();
cy.contains('button', 'Save').click();
flushClockAndSave();
assertNotification(notifications.error.missingField);
assertFieldErrorStatus('Authors', colorError);
cy.get('div[class*=SortableListItem]')
Expand All @@ -500,10 +500,10 @@ function validateListFields({ name, description }) {
});
assertListControlErrorStatus([colorError, colorError], '@listControl');
cy.get('input')
.eq(2)
.last()
.type(name);
cy.getMarkdownEditor()
.eq(2)
.last()
.type(description);
flushClockAndSave();
assertNotification(notifications.saved);
Expand Down Expand Up @@ -718,4 +718,5 @@ module.exports = {
assertPublishedEntryInEditor,
assertUnpublishedEntryInEditor,
assertUnpublishedChangesInEditor,
advanceClock,
};
10 changes: 5 additions & 5 deletions packages/decap-cms-widget-code/src/CodeControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ClassNames } from '@emotion/react';
import { Map } from 'immutable';
import { uniq, isEqual, isEmpty } from 'lodash';
import { uniq, isEqual, isEmpty, debounce } from 'lodash';
import { v4 as uuid } from 'uuid';
import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
import CodeMirror from 'codemirror';
Expand Down Expand Up @@ -115,6 +115,8 @@ export default class CodeControl extends React.Component {
}
}

debounceOnChange = debounce(value => this.props.onChange(value), 300);

updateCodeMirrorProps(prevState) {
const keys = ['lang', 'theme', 'keyMap'];
const changedProps = getChangedProps(prevState, this.state, keys);
Expand Down Expand Up @@ -188,8 +190,6 @@ export default class CodeControl extends React.Component {
}

async handleChangeCodeMirrorProps(changedProps) {
const { onChange } = this.props;

if (changedProps.lang) {
const { mode } = this.getLanguageByName(changedProps.lang) || {};
if (mode) {
Expand Down Expand Up @@ -218,15 +218,15 @@ export default class CodeControl extends React.Component {
// Only persist the language change if supported - requires the value to be
// a map rather than just a code string.
if (changedProps.lang && this.valueIsMap()) {
onChange(this.toValue('lang', changedProps.lang));
this.debounceOnChange(this.toValue('lang', changedProps.lang));
}
}

handleChange(newValue) {
const cursor = this.cm.doc.getCursor();
const selections = this.cm.doc.listSelections();
this.setState({ lastKnownValue: newValue });
this.props.onChange(this.toValue('code', newValue), { cursor, selections });
this.debounceOnChange(this.toValue('code', newValue), { cursor, selections });
}

showSettings = () => {
Expand Down
35 changes: 27 additions & 8 deletions packages/decap-cms-widget-colorstring/src/ColorControl.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled from '@emotion/styled';
import ChromePicker from 'react-color';
import tinycolor from 'tinycolor2';
import { zIndex } from 'decap-cms-ui-default';
import { debounce } from 'lodash';

function ClearIcon() {
return (
Expand Down Expand Up @@ -78,6 +80,7 @@ const ClickOutsideDiv = styled.div`

export default class ColorControl extends React.Component {
static propTypes = {
field: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
forID: PropTypes.string,
value: PropTypes.node,
Expand All @@ -91,28 +94,44 @@ export default class ColorControl extends React.Component {
};

state = {
value: this.props.value,
showColorPicker: false,
};

debounceOnChange = debounce(value => this.props.onChange(value), 300);

// show/hide color picker
handleClick = () => {
this.setState({ showColorPicker: !this.state.showColorPicker });
};

handleClear = () => {
this.props.onChange('');
this.setState({ value: '' });
this.debounceOnChange('');
};

handleClose = () => {
this.setState({ showColorPicker: false });
};

handleInputChange = e => {
const { value } = e.target;
this.setState({ value });
this.debounceOnChange(value);
};

handleChange = color => {
const formattedColor =
color.rgb.a < 1
? `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`
: color.hex;
this.props.onChange(formattedColor);
this.setState({ value: formattedColor });
this.debounceOnChange(formattedColor);
};

render() {
const { forID, value, field, onChange, classNameWrapper, setActiveStyle, setInactiveStyle } =
this.props;
const { forID, field, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props;
const { value } = this.state;

const allowInput = field.get('allowInput', false);

Expand All @@ -123,7 +142,7 @@ export default class ColorControl extends React.Component {
<>
{' '}
{showClearButton && (
<ClearButtonWrapper>
<ClearButtonWrapper data-testid="clear-btn-wrapper">
<ClearButton onClick={this.handleClear}>
<ClearIcon />
</ClearButton>
Expand All @@ -140,8 +159,8 @@ export default class ColorControl extends React.Component {
?
</ColorSwatch>
{this.state.showColorPicker && (
<ColorPickerContainer>
<ClickOutsideDiv onClick={this.handleClose} />
<ColorPickerContainer data-testid="color-picker-container">
<ClickOutsideDiv onClick={this.handleClose} data-testid="picker-bg" />
<ChromePicker
color={value || ''}
onChange={this.handleChange}
Expand All @@ -155,7 +174,7 @@ export default class ColorControl extends React.Component {
id={forID}
className={classNameWrapper}
value={value || ''}
onChange={e => onChange(e.target.value)}
onChange={this.handleInputChange}
onFocus={setActiveStyle}
onBlur={setInactiveStyle}
style={{
Expand Down
14 changes: 10 additions & 4 deletions packages/decap-cms-widget-datetime/src/DateTimeControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import utc from 'dayjs/plugin/utc';
import { buttons } from 'decap-cms-ui-default';
import { debounce } from 'lodash';

dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);
Expand Down Expand Up @@ -98,6 +99,7 @@ class DateTimeControl extends React.Component {
isUtc = this.props.field.get('picker_utc') || false;
isValidDate = datetime => dayjs(datetime).isValid() || datetime === '';
defaultValue = this.getDefaultValue();
state = { value: this.defaultValue };

componentDidMount() {
const { value } = this.props;
Expand All @@ -124,17 +126,20 @@ class DateTimeControl extends React.Component {
return this.isUtc ? dayjs.utc(value).format(inputFormat) : dayjs(value).format(inputFormat);
}

onChange = debounce(value => this.props.onChange(value), 300);

handleChange = datetime => {
if (!this.isValidDate(datetime)) return;
const { onChange } = this.props;

if (datetime === '') {
onChange('');
this.onChange('');
} else {
const { format, inputFormat } = this.getFormat();
const formattedValue = dayjs(datetime, inputFormat).format(format);
onChange(formattedValue);
this.onChange(formattedValue);
}

this.setState({ value: datetime });
};

onInputChange = e => {
Expand All @@ -144,8 +149,9 @@ class DateTimeControl extends React.Component {
};

render() {
const { forID, value, classNameWrapper, setActiveStyle, setInactiveStyle, t, isDisabled } =
const { forID, classNameWrapper, setActiveStyle, setInactiveStyle, t, isDisabled } =
this.props;
const { value } = this.state
const { inputType, inputFormat } = this.getFormat();

return (
Expand Down
17 changes: 12 additions & 5 deletions packages/decap-cms-widget-number/src/NumberControl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
const ValidationErrorTypes = {
PRESENCE: 'PRESENCE',
PATTERN: 'PATTERN',
Expand Down Expand Up @@ -68,15 +69,20 @@ export default class NumberControl extends React.Component {
value: '',
};

state = { value: this.props.value };

debounceOnChange = debounce(value => this.props.onChange(value), 300);

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);
if (isNaN(value)) {
this.setState({ value: '' });
this.debounceOnChange('');
} else {
onChange('');
this.setState({ value });
this.debounceOnChange(value);
}
};

Expand All @@ -96,7 +102,8 @@ export default class NumberControl extends React.Component {
};

render() {
const { field, value, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props;
const { field, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props;
const { value } = this.state;
const min = field.get('min', '');
const max = field.get('max', '');
const step = field.get('step', field.get('value_type') === 'int' ? 1 : '');
Expand Down
12 changes: 12 additions & 0 deletions packages/decap-cms-widget-number/src/__tests__/number.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function setup({ field, defaultValue }) {

const input = helpers.container.querySelector('input');

jest.useFakeTimers();

return {
...helpers,
...renderArgs,
Expand All @@ -82,6 +84,8 @@ describe('Number widget', () => {
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });

jest.runAllTimers();

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(testValue);
});
Expand All @@ -93,6 +97,8 @@ describe('Number widget', () => {
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '' } });

jest.runAllTimers();

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith('');
});
Expand All @@ -104,6 +110,8 @@ describe('Number widget', () => {
fireEvent.focus(input);
fireEvent.change(input, { target: { value: 'invalid' } });

jest.runAllTimers();

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith('');
});
Expand All @@ -116,6 +124,8 @@ describe('Number widget', () => {
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });

jest.runAllTimers();

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(parseInt(testValue, 10));
});
Expand All @@ -128,6 +138,8 @@ describe('Number widget', () => {
fireEvent.focus(input);
fireEvent.change(input, { target: { value: String(testValue) } });

jest.runAllTimers();

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(parseFloat(testValue));
});
Expand Down
Loading
Loading