Skip to content

Commit

Permalink
Fix: improve datetime widget (#7261)
Browse files Browse the repository at this point in the history
- don't display the current date by default or when the field is empty (#3679) - **potentially breaking change**
- add `default: '{{now}}'` option to enable the current behavior of displaying the current time (using the specified format)
- add UTC indicator when `picker_utc: true`
- improve how `Z` in format and `picker_utc: true` work together
- reorder format importance: if `format` is set, `date_format` and `time_format` (if strings) are ignored (#7250)

BREAKING CHANGE: The datetime field is empty by default, from now on, but it was prefilled with the current date until now. Use `default: '{{now}}'` to prefill the field with the current date.
  • Loading branch information
martinjagodic authored Aug 12, 2024
1 parent f16f736 commit 94993be
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 50 deletions.
1 change: 1 addition & 0 deletions dev-test/backends/azure/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/bitbucket/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/git-gateway/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/gitea/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/github/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/gitlab/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
1 change: 1 addition & 0 deletions dev-test/backends/proxy/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ collections:
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm'
default: 1970-01-01T01:00
- label: Description
name: description
widget: text
Expand Down
3 changes: 1 addition & 2 deletions dev-test/backends/test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ collections: # A list of collections the CMS should be able to edit
label: 'Publish Date',
name: 'date',
widget: 'datetime',
date_format: 'YYYY-MM-DD',
time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm',
default: '{{now}}',
}
- label: 'Cover Image'
name: 'image'
Expand Down
3 changes: 1 addition & 2 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ collections: # A list of collections the CMS should be able to edit
label: 'Publish Date',
name: 'date',
widget: 'datetime',
date_format: 'YYYY-MM-DD',
time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm',
default: '{{now}}',
}
- label: 'Cover Image'
name: 'image'
Expand Down
110 changes: 64 additions & 46 deletions packages/decap-cms-widget-datetime/src/DateTimeControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);
dayjs.extend(utc);

function Buttons({ t, handleChange, inputFormat, isUtc }) {
function Buttons({ t, handleChange, getNow }) {
return (
<div
css={css`
Expand All @@ -26,9 +26,8 @@ function Buttons({ t, handleChange, inputFormat, isUtc }) {
${buttons.button}
${buttons.widget}
`}
onClick={() =>
handleChange(isUtc ? dayjs.utc().format(inputFormat) : dayjs().format(inputFormat))
}
onClick={() => handleChange(getNow())}
data-testid="now-button"
>
{t('editor.editorWidgets.datetime.now')}
</button>
Expand All @@ -38,6 +37,7 @@ function Buttons({ t, handleChange, inputFormat, isUtc }) {
${buttons.widget}
`}
onClick={() => handleChange('')}
data-testid="clear-button"
>
{t('editor.editorWidgets.datetime.clear')}
</button>
Expand All @@ -62,58 +62,71 @@ class DateTimeControl extends React.Component {
isDisabled: false,
};

isUtc = this.props.field.get('picker_utc') || false;

escapeZ(str) {
if (/Z(?![\]])/.test(str)) {
return str.replace('Z', '[Z]');
}
return str;
}

getFormat() {
const { field } = this.props;
const format = field?.get('format') || 'YYYY-MM-DDTHH:mm:ss.SSS[Z]';
const dateFormat = field?.get('date_format');
const timeFormat = field?.get('time_format');
let inputFormat = 'YYYY-MM-DDTHH:mm';
let inputType = 'datetime-local';

if (dateFormat && timeFormat) {
return { format: `${dateFormat}T${timeFormat}`, inputType, inputFormat };
let inputFormat = 'YYYY-MM-DDTHH:mm';
let format = 'YYYY-MM-DDTHH:mm:ss.SSS[Z]';
let userFormat = field?.get('format');
let dateFormat = field?.get('date_format');
let timeFormat = field?.get('time_format');
if (dateFormat === true) dateFormat = 'YYYY-MM-DD';
if (timeFormat === true) timeFormat = 'HH:mm';

if (this.isUtc) {
userFormat = this.escapeZ(userFormat);
dateFormat = this.escapeZ(dateFormat);
timeFormat = this.escapeZ(timeFormat);
}

if (timeFormat) {
if (typeof dateFormat === 'string' && typeof timeFormat === 'string') {
format = `${dateFormat}T${timeFormat}`;
} else if (typeof timeFormat === 'string') {
inputType = 'time';
inputFormat = 'HH:mm';
return { format: timeFormat, inputType, inputFormat };
format = timeFormat;
} else if (typeof dateFormat === 'string') {
inputType = 'date';
format = dateFormat;
}

if (dateFormat) {
inputType = 'date';
inputFormat = 'YYYY-MM-DD';
return { format: dateFormat, inputType, inputFormat };
if (typeof userFormat === 'string') {
format = userFormat;
inputType = 'datetime-local';
}

return { format, inputType, inputFormat };
}
if (dateFormat === false) inputType = 'time';
if (timeFormat === false) inputType = 'date';
if (inputType === 'datetime-local') inputFormat = 'YYYY-MM-DDTHH:mm';
if (inputType === 'date') inputFormat = 'YYYY-MM-DD';
if (inputType === 'time') inputFormat = 'HH:mm';

getDefaultValue() {
const { field } = this.props;
const defaultValue = field.get('default');
return defaultValue;
return { format, inputType, inputFormat };
}

isUtc = this.props.field.get('picker_utc') || false;
isValidDate = datetime => dayjs(datetime).isValid() || datetime === '';
defaultValue = this.getDefaultValue();
isValidDate = dt => dayjs(dt, this.getFormat().inputFormat).isValid() || dt === '';

componentDidMount() {
const { value } = this.props;
getNow() {
const { inputFormat } = this.getFormat();
if (value === undefined) {
setTimeout(() => {
this.handleChange(
this.defaultValue === undefined ? dayjs().format(inputFormat) : this.defaultValue,
);
}, 0);
}
return this.isUtc ? dayjs.utc().format(inputFormat) : dayjs().format(inputFormat);
}

formatInputValue(value) {
if (value === '') return value;
const { format, inputFormat } = this.getFormat();

if (typeof value === 'string' && value?.replace(/\s+/g, '') === '{{now}}') {
return this.getNow();
}

const inputValue = this.isUtc
? dayjs.utc(value, format).format(inputFormat)
: dayjs(value, format).format(inputFormat);
Expand All @@ -139,14 +152,13 @@ class DateTimeControl extends React.Component {

onInputChange = e => {
const etv = e.target.value;
const newValue = dayjs(etv);
this.handleChange(etv === '' ? '' : newValue);
this.handleChange(etv);
};

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

return (
<div
Expand All @@ -159,20 +171,26 @@ class DateTimeControl extends React.Component {
>
<input
id={forID}
data-testid={forID}
type={inputType}
value={this.formatInputValue(value)}
value={value ? this.formatInputValue(value) : ''}
onChange={this.onInputChange}
onFocus={setActiveStyle}
onBlur={setInactiveStyle}
disabled={isDisabled}
/>
{this.isUtc && (
<span
css={css`
font-size: 0.8em;
color: #666;
`}
>
UTC
</span>
)}
{!isDisabled && (
<Buttons
t={t}
handleChange={v => this.handleChange(v)}
inputFormat={inputFormat}
isUtc={this.isUtc}
/>
<Buttons t={t} handleChange={v => this.handleChange(v)} getNow={() => this.getNow()} />
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import dayjs from 'dayjs';

import DateTimeControl from '../DateTimeControl';

function setup(propsOverrides = {}) {
const props = {
forID: 'test-datetime',
onChange: jest.fn(),
classNameWrapper: 'classNameWrapper',
setActiveStyle: jest.fn(),
setInactiveStyle: jest.fn(),
value: '',
t: key => key,
isDisabled: false,
field: {
get: jest.fn().mockReturnValue('DD.MM.YYYY'),
},
...propsOverrides,
};

const utils = render(<DateTimeControl {...props} />);
const input = utils.getByTestId('test-datetime');
const nowButton = utils.getByTestId('now-button');
const clearButton = utils.getByTestId('clear-button');

return {
...utils,
props,
input,
nowButton,
clearButton,
};
}

describe('DateTimeControl', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('renders the component with input, now button, and clear button', () => {
const { getByTestId } = setup();
expect(getByTestId('test-datetime')).toBeInTheDocument();
expect(getByTestId('now-button')).toBeInTheDocument();
expect(getByTestId('clear-button')).toBeInTheDocument();
});

test('set value to current date if now button is clicked', () => {
const { nowButton, props } = setup();
fireEvent.click(nowButton);
expect(props.onChange).toHaveBeenCalledWith(dayjs().format('DD.MM.YYYY'));
});

test('set value to empty string if clear button is clicked', () => {
const { clearButton, props } = setup({ value: '1970-01-01' });
fireEvent.click(clearButton);
expect(props.onChange).toHaveBeenCalledWith('');
});
});

0 comments on commit 94993be

Please sign in to comment.