diff --git a/packages/@sanity/util/src/legacyDateFormat.ts b/packages/@sanity/util/src/legacyDateFormat.ts index b37d60d645bc..ec19091122ad 100644 --- a/packages/@sanity/util/src/legacyDateFormat.ts +++ b/packages/@sanity/util/src/legacyDateFormat.ts @@ -12,7 +12,9 @@ export type ParseResult = {isValid: boolean; date?: Date; error?: string} & ( // todo: find a way to get rid of moment there. // note: the format comes form peoples schemas, so we need to deprecate it for a while and // find a way to tell people that they need to change it -export function format(input: Date, format: string) { +export function format(input: Date, format: string, useUTC = false) { + if (useUTC) return moment.utc(input).format(format) + return moment(input).format(format) } diff --git a/packages/sanity/src/core/validation/validators/dateValidator.ts b/packages/sanity/src/core/validation/validators/dateValidator.ts index c781a9c6adc0..90a9ec78756c 100644 --- a/packages/sanity/src/core/validation/validators/dateValidator.ts +++ b/packages/sanity/src/core/validation/validators/dateValidator.ts @@ -17,7 +17,7 @@ interface DateTimeOptions { timeFormat?: string } -const getFormattedDate = (type = '', value: string | number | Date, options?: DateTimeOptions) => { +const getFormattedDate = (type = '', value: Date, options?: DateTimeOptions) => { const dateFormat = options?.dateFormat || legacyDateFormat.DEFAULT_DATE_FORMAT const timeFormat = options?.timeFormat || legacyDateFormat.DEFAULT_TIME_FORMAT @@ -25,8 +25,9 @@ const getFormattedDate = (type = '', value: string | number | Date, options?: Da // instead of it being assumed to be UTC. This was a problem because midnight UTC is the previous // day in many other timezones resulting in the date displayed to be the previous day. return legacyDateFormat.format( - new Date(type === 'date' ? `${value}T00:00:00` : value), + value, type === 'date' ? dateFormat : `${dateFormat} ${timeFormat}`, + type === 'date', ) } @@ -59,11 +60,13 @@ export const dateValidators: Validators = { min: (minDate, value, message, {type, i18n}) => { const dateVal = parseDate(value) + const minDateVal = parseDate(minDate, true) + if (!dateVal) { return true // `type()` should catch parse errors } - if (!value || dateVal >= parseDate(minDate, true)) { + if (!value || dateVal >= minDateVal) { return true } @@ -81,7 +84,7 @@ export const dateValidators: Validators = { // validator is available as `providedMinDate`. This because the formatted date is likely // what the developer wants to present to the user i18n.t('validation:date.minimum', { - minDate: getFormattedDate(type.name, minDate, dateTimeOptions), + minDate: getFormattedDate(type.name, minDateVal, dateTimeOptions), providedMinDate: minDate, }) ) @@ -89,11 +92,13 @@ export const dateValidators: Validators = { max: (maxDate, value, message, {type, i18n}) => { const dateVal = parseDate(value) + const maxDateVal = parseDate(maxDate, true) + if (!dateVal) { return true // `type()` should catch parse errors } - if (!value || dateVal <= parseDate(maxDate, true)) { + if (!value || dateVal <= maxDateVal) { return true } @@ -111,7 +116,7 @@ export const dateValidators: Validators = { // validator is available as `providedMaxDate`. This because the formatted date is likely // what the developer wants to present to the user i18n.t('validation:date.maximum', { - maxDate: getFormattedDate(type.name, maxDate, dateTimeOptions), + maxDate: getFormattedDate(type.name, maxDateVal, dateTimeOptions), providedMaxDate: maxDate, }) ) diff --git a/packages/sanity/test/validation/__snapshots__/dates.test.ts.snap b/packages/sanity/test/validation/__snapshots__/dates.test.ts.snap index d2602f1bd4f0..fad5548e855d 100644 --- a/packages/sanity/test/validation/__snapshots__/dates.test.ts.snap +++ b/packages/sanity/test/validation/__snapshots__/dates.test.ts.snap @@ -1,23 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`date max length constraint: Must be at or before 1`] = ` +exports[`date with custom format max length constraint: Must be at or before 1`] = ` Array [ Object { "item": Object { - "message": "Must be at or before 2024-01-01", + "message": "Must be at or before 01-01-2024", }, "level": "error", - "message": "Must be at or before 2024-01-01", + "message": "Must be at or before 01-01-2024", "path": Array [], }, ] `; -exports[`date max length constraint: max length: valid 1`] = `Array []`; +exports[`date with custom format min length constraint: Must be at or after 1`] = ` +Array [ + Object { + "item": Object { + "message": "Must be at or after 01-01-2024", + }, + "level": "error", + "message": "Must be at or after 01-01-2024", + "path": Array [], + }, +] +`; -exports[`date max length constraint: max length: valid 2`] = `Array []`; +exports[`date with default format max length constraint: Must be at or before 1`] = ` +Array [ + Object { + "item": Object { + "message": "Must be at or before 2024-01-01", + }, + "level": "error", + "message": "Must be at or before 2024-01-01", + "path": Array [], + }, +] +`; -exports[`date min length constraint: Must be at or after 1`] = ` +exports[`date with default format min length constraint: Must be at or after 1`] = ` Array [ Object { "item": Object { @@ -30,40 +52,54 @@ Array [ ] `; -exports[`date min length constraint: min length: valid 1`] = `Array []`; - -exports[`date min length constraint: min length: valid 2`] = `Array []`; - -exports[`date with custom format max length constraint: Must be at or before 1`] = ` +exports[`datetime with custom format max length constraint: Must be at or before 1`] = ` Array [ Object { "item": Object { - "message": "Must be at or before 01-01-2024", + "message": "Must be at or before 1st. January 2024 09:31", }, "level": "error", - "message": "Must be at or before 01-01-2024", + "message": "Must be at or before 1st. January 2024 09:31", "path": Array [], }, ] `; -exports[`date with custom format max length constraint: max length: valid 1`] = `Array []`; - -exports[`date with custom format max length constraint: max length: valid 2`] = `Array []`; - -exports[`date with custom format min length constraint: Must be at or after 1`] = ` +exports[`datetime with custom format min length constraint: Must be at or after 1`] = ` Array [ Object { "item": Object { - "message": "Must be at or after 01-01-2024", + "message": "Must be at or after 1st. January 2024 09:31", }, "level": "error", - "message": "Must be at or after 01-01-2024", + "message": "Must be at or after 1st. January 2024 09:31", "path": Array [], }, ] `; -exports[`date with custom format min length constraint: min length: valid 1`] = `Array []`; +exports[`datetime with default format max length constraint: Must be at or before 1`] = ` +Array [ + Object { + "item": Object { + "message": "Must be at or before 2024-01-01 09:31", + }, + "level": "error", + "message": "Must be at or before 2024-01-01 09:31", + "path": Array [], + }, +] +`; -exports[`date with custom format min length constraint: min length: valid 2`] = `Array []`; +exports[`datetime with default format min length constraint: Must be at or after 1`] = ` +Array [ + Object { + "item": Object { + "message": "Must be at or after 2024-01-01 09:31", + }, + "level": "error", + "message": "Must be at or after 2024-01-01 09:31", + "path": Array [], + }, +] +`; diff --git a/packages/sanity/test/validation/dates.test.ts b/packages/sanity/test/validation/dates.test.ts index dca5e1008c61..406589b8ecc0 100644 --- a/packages/sanity/test/validation/dates.test.ts +++ b/packages/sanity/test/validation/dates.test.ts @@ -2,49 +2,101 @@ import {getFallbackLocaleSource} from '../../src/core/i18n/fallback' import {Rule} from '../../src/core/validation' describe('date', () => { - const context: any = {client: {}, i18n: getFallbackLocaleSource(), type: {name: 'date'}} - - test('min length constraint', async () => { - const rule = Rule.dateTime().min('2024-01-01') - await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot( - 'Must be at or after', - ) - await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot('min length: valid') - await expect(rule.validate('2024-01-01', context)).resolves.toMatchSnapshot('min length: valid') + describe('with default format', () => { + const context: any = {client: {}, i18n: getFallbackLocaleSource(), type: {name: 'date'}} + + test('min length constraint', async () => { + const rule = Rule.dateTime().min(Date.parse('2024-01-01')) + await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot( + 'Must be at or after', + ) + await expect(rule.validate('2024-01-02', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01', context)).resolves.toHaveLength(0) + }) + + test('max length constraint', async () => { + const rule = Rule.dateTime().max(Date.parse('2024-01-01')) + await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot( + 'Must be at or before', + ) + await expect(rule.validate('2023-12-31', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01', context)).resolves.toHaveLength(0) + }) }) - test('max length constraint', async () => { - const rule = Rule.dateTime().max('2024-01-01') - await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot( - 'Must be at or before', - ) - await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot('max length: valid') - await expect(rule.validate('2024-01-01', context)).resolves.toMatchSnapshot('max length: valid') + describe('with custom format', () => { + const context: any = { + client: {}, + i18n: getFallbackLocaleSource(), + type: {name: 'date', options: {dateFormat: 'MM-DD-YYYY'}}, + } + + test('min length constraint', async () => { + const rule = Rule.dateTime().min(Date.parse('2024-01-01')) + await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot( + 'Must be at or after', + ) + await expect(rule.validate('2024-01-02', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01', context)).resolves.toHaveLength(0) + }) + + test('max length constraint', async () => { + const rule = Rule.dateTime().max(Date.parse('2024-01-01')) + await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot( + 'Must be at or before', + ) + await expect(rule.validate('2023-12-31', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01', context)).resolves.toHaveLength(0) + }) }) }) -describe('date with custom format', () => { - const context: any = { - client: {}, - i18n: getFallbackLocaleSource(), - type: {name: 'date', options: {dateFormat: 'MM-DD-YYYY'}}, - } - - test('min length constraint', async () => { - const rule = Rule.dateTime().min('2024-01-01') - await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot( - 'Must be at or after', - ) - await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot('min length: valid') - await expect(rule.validate('2024-01-01', context)).resolves.toMatchSnapshot('min length: valid') +describe('datetime', () => { + describe('with default format', () => { + const context: any = {client: {}, i18n: getFallbackLocaleSource(), type: {name: 'datetime'}} + + test('min length constraint', async () => { + const rule = Rule.dateTime().min(Date.parse('2024-01-01T17:31:00.000Z')) + await expect(rule.validate('2023-12-31T17:31:00.000Z', context)).resolves.toMatchSnapshot( + 'Must be at or after', + ) + await expect(rule.validate('2024-01-02T17:31:00.000Z', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01T17:31:00.000Z', context)).resolves.toHaveLength(0) + }) + + test('max length constraint', async () => { + const rule = Rule.dateTime().max(Date.parse('2024-01-01T17:31:00.000Z')) + await expect(rule.validate('2024-01-02T17:31:00.000Z', context)).resolves.toMatchSnapshot( + 'Must be at or before', + ) + await expect(rule.validate('2023-12-23T17:31:00.000Z', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01T17:31:00.000Z', context)).resolves.toHaveLength(0) + }) }) - test('max length constraint', async () => { - const rule = Rule.dateTime().max('2024-01-01') - await expect(rule.validate('2024-01-02', context)).resolves.toMatchSnapshot( - 'Must be at or before', - ) - await expect(rule.validate('2023-12-31', context)).resolves.toMatchSnapshot('max length: valid') - await expect(rule.validate('2024-01-01', context)).resolves.toMatchSnapshot('max length: valid') + describe('with custom format', () => { + const context: any = { + client: {}, + i18n: getFallbackLocaleSource(), + type: {name: 'datetime', options: {dateFormat: 'Do. MMMM YYYY'}}, + } + + test('min length constraint', async () => { + const rule = Rule.dateTime().min(Date.parse('2024-01-01T17:31:00.000Z')) + await expect(rule.validate('2023-12-31T17:31:00.000Z', context)).resolves.toMatchSnapshot( + 'Must be at or after', + ) + await expect(rule.validate('2024-01-02T17:31:00.000Z', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01T17:31:00.000Z', context)).resolves.toHaveLength(0) + }) + + test('max length constraint', async () => { + const rule = Rule.dateTime().max(Date.parse('2024-01-01T17:31:00.000Z')) + await expect(rule.validate('2024-01-02T17:31:00.000Z', context)).resolves.toMatchSnapshot( + 'Must be at or before', + ) + await expect(rule.validate('2023-12-31T17:31:00.000Z', context)).resolves.toHaveLength(0) + await expect(rule.validate('2024-01-01T17:31:00.000Z', context)).resolves.toHaveLength(0) + }) }) })