diff --git a/fields/types/datetime/DatetimeField.js b/fields/types/datetime/DatetimeField.js index 55d4b724c6..cb22c08ea1 100644 --- a/fields/types/datetime/DatetimeField.js +++ b/fields/types/datetime/DatetimeField.js @@ -20,25 +20,33 @@ module.exports = Field.create({ focusTargetRef: 'dateInput', - // default input formats - dateInputFormat: 'YYYY-MM-DD', - timeInputFormat: 'h:mm:ss a', - tzOffsetInputFormat: 'Z', - // parse formats (duplicated from lib/fieldTypes/datetime.js) parseFormats: ['YYYY-MM-DD', 'YYYY-MM-DD h:m:s a', 'YYYY-MM-DD h:m a', 'YYYY-MM-DD H:m:s', 'YYYY-MM-DD H:m'], getInitialState () { return { - dateValue: this.props.value && this.moment(this.props.value).format(this.dateInputFormat), - timeValue: this.props.value && this.moment(this.props.value).format(this.timeInputFormat), - tzOffsetValue: this.props.value ? this.moment(this.props.value).format(this.tzOffsetInputFormat) : this.moment().format(this.tzOffsetInputFormat), + dateValue: this.props.value && this.moment(this.props.value).format(this.getDateInputFormat()), + timeValue: this.props.value && this.moment(this.props.value).format(this.getTimeInputFormat()), + tzOffsetValue: this.props.value ? this.moment(this.props.value).format(this.getTzInputFormat()) : this.moment().format(this.getTzInputFormat()), }; }, + getDateInputFormat () { + return this.props.formatDateString; + }, + + getTimeInputFormat () { + return this.props.formatTimeString; + }, + + getTzInputFormat () { + return this.props.formatTzString; + }, + getDefaultProps () { return { - formatString: 'Do MMM YYYY, h:mm:ss a', + formatDateString: 'YYYY-MM-DD', + formatTimeString: 'h:mm:ss a', }; }, @@ -54,22 +62,22 @@ module.exports = Field.create({ // TODO: Move format() so we can share with server-side code format (value, format) { - format = format || this.dateInputFormat + ' ' + this.timeInputFormat; + format = format || this.getDateInputFormat() + ' ' + this.getTimeInputFormat(); return value ? this.moment(value).format(format) : ''; }, handleChange (dateValue, timeValue, tzOffsetValue) { var value = dateValue + ' ' + timeValue; - var datetimeFormat = this.dateInputFormat + ' ' + this.timeInputFormat; + var datetimeFormat = this.getDateInputFormat() + ' ' + this.getTimeInputFormat(); // if the change included a timezone offset, include that in the calculation (so NOW works correctly during DST changes) if (typeof tzOffsetValue !== 'undefined') { value += ' ' + tzOffsetValue; - datetimeFormat += ' ' + this.tzOffsetInputFormat; + datetimeFormat += ' ' + this.getTzInputFormat(); } // if not, calculate the timezone offset based on the date (respect different DST values) else { - this.setState({ tzOffsetValue: this.moment(value, datetimeFormat).format(this.tzOffsetInputFormat) }); + this.setState({ tzOffsetValue: this.moment(value, datetimeFormat).format(this.getTzInputFormat()) }); } this.props.onChange({ @@ -89,9 +97,9 @@ module.exports = Field.create({ }, setNow () { - var dateValue = this.moment().format(this.dateInputFormat); - var timeValue = this.moment().format(this.timeInputFormat); - var tzOffsetValue = this.moment().format(this.tzOffsetInputFormat); + var dateValue = this.moment().format(this.getDateInputFormat()); + var timeValue = this.moment().format(this.getTimeInputFormat()); + var tzOffsetValue = this.moment().format(this.getTzInputFormat()); this.setState({ dateValue: dateValue, timeValue: timeValue, @@ -113,7 +121,7 @@ module.exports = Field.create({
diff --git a/fields/types/datetime/DatetimeType.js b/fields/types/datetime/DatetimeType.js index 0b273eebbc..d1e1058b87 100644 --- a/fields/types/datetime/DatetimeType.js +++ b/fields/types/datetime/DatetimeType.js @@ -6,6 +6,7 @@ var utils = require('keystone-utils'); // ISO_8601 is needed for the automatically created createdAt and updatedAt fields var parseFormats = ['YYYY-MM-DD', 'YYYY-MM-DD h:m:s a', 'YYYY-MM-DD h:m a', 'YYYY-MM-DD H:m:s', 'YYYY-MM-DD H:m', 'YYYY-MM-DD h:mm:s a Z', moment.ISO_8601]; + /** * DateTime FieldType Constructor * @extends Field @@ -15,14 +16,44 @@ function datetime (list, path, options) { this._nativeType = Date; this._underscoreMethods = ['format', 'moment', 'parse']; this._fixedSize = 'full'; - this._properties = ['formatString', 'isUTC']; + this._properties = ['formatDateString', 'formatTimeString', 'formatTzString', 'isUTC']; this.typeDescription = 'date and time'; - this.parseFormatString = options.parseFormat || parseFormats; - this.formatString = (options.format === false) ? false : (options.format || 'YYYY-MM-DD h:mm:ss a'); + this.parseFormatString = parseFormats.slice(0); + this.formatDateString = (options.dateFormat === false) ? false : (options.dateFormat || 'YYYY-MM-DD'); + this.formatTimeString = (options.timeFormat === false) ? false : (options.timeFormat || 'h:mm:ss a'); + this.formatTzString = (options.tzFormat === false) ? false : (options.tzFormat || 'Z'); this.isUTC = options.utc || false; - if (this.formatString && typeof this.formatString !== 'string') { - throw new Error('FieldType.DateTime: options.format must be a string.'); + if (this.formatDateString && typeof this.formatDateString !== 'string') { + throw new Error('FieldType.DateTime: options.dateFormat must be a string.'); + } + if (this.formatTimeString && typeof this.formatTimeString !== 'string') { + throw new Error('FieldType.DateTime: options.timeFormat must be a string.'); + } + if (this.formatTzString && typeof this.formatTzString !== 'string') { + throw new Error('FieldType.DateTime: options.tzFormat must be a string.'); + } + + // For backward compatibility, if parseFormat option is specified, add it to the parseFormatString array + if (options.parseFormat) { + if (Array.isArray(options.parseFormat)) { + this.parseFormatString = this.parseFormatString.concat(options.parseFormat); + } else if (typeof options.parseFormat === 'string') { + this.parseFormatString.push(options.parseFormat); + } } + + // If a custom format is specified by the user, it should be added to the parseFormatString array to ensure + // successful validation + if (this.formatDateString || this.formatTimeString || this.formatTzString) { + let customFormat = []; + + if (this.formatDateString) customFormat.push(this.formatDateString); + if (this.formatTimeString) customFormat.push(this.formatTimeString); + if (this.formatTzString) customFormat.push(this.formatTzString); + + this.parseFormatString.push(customFormat.join(' ')); + } + datetime.super_.call(this, list, path, options); this.paths = { date: this.path + '_date', @@ -30,6 +61,7 @@ function datetime (list, path, options) { tzOffset: this.path + '_tzOffset', }; } + datetime.properName = 'Datetime'; util.inherits(datetime, FieldType); @@ -57,13 +89,13 @@ datetime.prototype.getInputFromData = function (data) { return this.getValueFromData(data); }; - datetime.prototype.validateRequiredInput = function (item, data, callback) { var value = this.getInputFromData(data); var result = !!value; if (value === undefined && item.get(this.path)) { result = true; } + utils.defer(callback, result); }; @@ -79,6 +111,7 @@ datetime.prototype.validateInput = function (data, callback) { if (value) { result = this.parse(value, this.parseFormatString, true).isValid(); } + utils.defer(callback, result); }; @@ -114,7 +147,7 @@ datetime.prototype.updateItem = function (item, data, callback) { if (!item.get(this.path) || !newValue.isSame(item.get(this.path))) { item.set(this.path, newValue.toDate()); } - // If it's null or empty string, clear it out + // If it's null or empty string, clear it out } else { item.set(this.path, null); } diff --git a/fields/types/datetime/Readme.md b/fields/types/datetime/Readme.md index d408de5a87..026b9ef608 100644 --- a/fields/types/datetime/Readme.md +++ b/fields/types/datetime/Readme.md @@ -7,7 +7,8 @@ Internally uses [moment.js](http://momentjs.com/) to manage date parsing, format If the `utc` option is set, `moment(value).utc()` is called in all methods to enable moment's utc mode. -String parsing with moment will be done using the `parseFormat` option, which defaults to `"'YYYY-MM-DD h:m:s a'"`. +String parsing with moment will be done using the `dateFormat`, `timeFormat` and `tzFormat` options which default to +`'YYYY-MM-DD'`, `'h:mm:ss a'` and `'Z'` respectively. ## Example @@ -19,18 +20,33 @@ String parsing with moment will be done using the `parseFormat` option, which de * `parseFormat` `string` -The default pattern to read in values with. Defaults to an array of values to try: +The default pattern to read in values with. This pattern is added to the below array of default values along with the +format specified in the `dateFormat`, `timeFormat` and `tzFormat` options. + +This option option need only be specified if you require format(s) that don't appear below and don't match the display +format. `['YYYY-MM-DD', 'YYYY-MM-DD h:m:s a', 'YYYY-MM-DD h:m a', 'YYYY-MM-DD H:m:s', 'YYYY-MM-DD H:m', 'YYYY-MM-DD h:mm:s a Z', moment.ISO_8601]` +* `dateFormat` `string` + +The default format pattern to use when displaying the date portion of the value. Defaults to `YYYY-MM-DD` + +See the [momentjs format docs](http://momentjs.com/docs/#/displaying/format/) for information on the supported formats and options. + +* `timeFormat` `string` + +The default format pattern to use when displaying the time portion of the value. Defaults to `h:mm:ss a` + +See the [momentjs format docs](http://momentjs.com/docs/#/displaying/format/) for information on the supported formats and options. -* `format` `string` +* `dateFormat` `string` -The default format pattern to use when display the information. Defaults to `Do MMM YYYY hh:mm:ss a` +The default format pattern to use when displaying the timezone offset portion of the value. Defaults to `Z` See the [momentjs format docs](http://momentjs.com/docs/#/displaying/format/) for information on the supported formats and options. -`utc` `boolean` +* `utc` `boolean` Sets whether the string should be displayed in the admin UI in UTC time or local time. Defaults to `false`. diff --git a/fields/types/datetime/test/type.js b/fields/types/datetime/test/type.js index 2d3fab7f12..062733c62a 100644 --- a/fields/types/datetime/test/type.js +++ b/fields/types/datetime/test/type.js @@ -6,6 +6,11 @@ var DatetimeType = require('../DatetimeType'); exports.initList = function (List) { List.add({ datetime: DatetimeType, + customDisplayFormat: { + type: DatetimeType, + dateFormat: 'D MMM YYYY', + timeFormat: 'HH:mm', + }, customFormat: { type: DatetimeType, parseFormat: 'DD.MM.YY h:m a', @@ -18,13 +23,47 @@ exports.initList = function (List) { exports.testFieldType = function (List) { describe('invalid options', function () { - it('should throw when format is not a string', function (done) { + it('should throw when dateFormat is not a string', function (done) { + try { + List.add({ + invalidFormatOption: { type: DatetimeType, dateFormat: /aregexp/ }, + }); + + // If control reaches here, exception has not been thrown. Test failed. + demand(true).not.eql(true); + done(); + } catch (err) { + demand(err.message).eql('FieldType.DateTime: options.dateFormat must be a string.'); + done(); + } + }); + + it('should throw when timeFormat is not a string', function (done) { + try { + List.add({ + invalidFormatOption: { type: DatetimeType, timeFormat: /aregexp/ }, + }); + + // If control reaches here, exception has not been thrown. Test failed. + demand(true).not.eql(true); + done(); + } catch (err) { + demand(err.message).eql('FieldType.DateTime: options.timeFormat must be a string.'); + done(); + } + }); + + it('should throw when tzFormat is not a string', function (done) { try { List.add({ - invalidFormatOption: { type: DatetimeType, format: /aregexp/ }, + invalidFormatOption: { type: DatetimeType, tzFormat: /aregexp/ }, }); + + // If control reaches here, exception has not been thrown. Test failed. + demand(true).not.eql(true); + done(); } catch (err) { - demand(err.message).eql('FieldType.DateTime: options.format must be a string.'); + demand(err.message).eql('FieldType.DateTime: options.tzFormat must be a string.'); done(); } }); @@ -169,6 +208,15 @@ exports.testFieldType = function (List) { }); }); + it('should validate a date time string in a custom format when a custom display format is specified', function (done) { + List.fields.customDisplayFormat.validateInput({ + customDisplayFormat: '20 Jan 2018 14:00 +00:00', + }, function (result) { + demand(result).be.true(); + done(); + }); + }); + it('should validate a date time string in a custom format when specified', function (done) { List.fields.customFormat.validateInput({ customFormat: '25.02.16 04:45 am', @@ -187,11 +235,11 @@ exports.testFieldType = function (List) { }); }); - it('should invalidate a date time string in the default format when a custom one is specified', function (done) { + it('should validate a date time string in the default format when a custom one is specified', function (done) { List.fields.customFormat.validateInput({ customFormat: '2016-02-25 04:45:00 am', }, function (result) { - demand(result).be.false(); + demand(result).be.true(); done(); }); });