From c5bd734b5e4fc54f7d12a386c94051fdfb8c6425 Mon Sep 17 00:00:00 2001 From: Boon Xian Date: Wed, 1 Apr 2026 16:08:44 +0800 Subject: [PATCH] [MOL-20629][BX] update schema for date-range and test case --- src/__tests__/fields/date-range-field.spec.ts | 209 +++++++++ .../date-range-field/date-range-field.ts | 397 ++++++++++++------ src/fields/date-range-field/types.ts | 1 + src/schema-generator/yup-helper.ts | 12 +- src/shared/error-messages.ts | 6 +- 5 files changed, 494 insertions(+), 131 deletions(-) diff --git a/src/__tests__/fields/date-range-field.spec.ts b/src/__tests__/fields/date-range-field.spec.ts index b464abb..39d2d25 100644 --- a/src/__tests__/fields/date-range-field.spec.ts +++ b/src/__tests__/fields/date-range-field.spec.ts @@ -81,6 +81,215 @@ describe("date-range-field", () => { ).toBe(ERROR_MESSAGES.DATE_RANGE.INVALID); }); + describe("dateFormat", () => { + it("should support custom date format error message", () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + field: { + uiType: "date-range-field", + validation: [{ dateFormat: true, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + }); + + expect( + TestHelper.getError(() => schema.validateSync({ field: { from: "invalid from", to: "invalid to" } })) + .message + ).toBe(ERROR_MESSAGE); + }); + + it("should use default error message if dateFormat rule has no errorMessage", () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + field: { + uiType: "date-range-field", + validation: [{ dateFormat: true }], + }, + }, + }, + }); + + expect( + TestHelper.getError(() => schema.validateSync({ field: { from: "invalid from", to: "invalid to" } })) + .message + ).toBe(ERROR_MESSAGES.DATE_RANGE.INVALID); + }); + + it("should pass validation for valid dates even with dateFormat rule", () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + field: { + uiType: "date-range-field", + validation: [{ dateFormat: true, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + }); + + expect(() => schema.validateSync({ field: { from: "2023-01-01", to: "2023-02-01" } })).not.toThrowError(); + }); + }); + + describe("when condition", () => { + const CONDITION_FIELD_ID = "condition-field"; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.spyOn(LocalDate, "now").mockReturnValue(LocalDate.parse("2023-01-01")); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should validate as required when condition is met", () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + [CONDITION_FIELD_ID]: { + uiType: "text-field", + }, + field: { + uiType: "date-range-field", + validation: [ + { + when: { + [CONDITION_FIELD_ID]: { + is: [{ filled: true }], + then: [{ required: true, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + ], + }, + }, + }, + }); + + expect( + TestHelper.getError(() => + schema.validateSync({ + [CONDITION_FIELD_ID]: "hello", + field: { from: undefined, to: undefined }, + }) + ).message + ).toBe(ERROR_MESSAGE); + }); + + it("should not validate as required when condition is not met", () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + [CONDITION_FIELD_ID]: { + uiType: "text-field", + }, + field: { + uiType: "date-range-field", + validation: [ + { + when: { + [CONDITION_FIELD_ID]: { + is: [{ filled: true }], + then: [{ required: true, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + ], + }, + }, + }, + }); + + expect(() => + schema.validateSync({ + [CONDITION_FIELD_ID]: "", + field: { from: undefined, to: undefined }, + }) + ).not.toThrowError(); + }); + + describe.each` + rule | ruleValue | invalid | valid + ${"future"} | ${true} | ${{ from: "2021-01-01", to: "2022-01-01" }} | ${{ from: "2023-01-02", to: "2023-01-03" }} + ${"past"} | ${true} | ${{ from: "2023-01-01", to: "2024-01-01" }} | ${{ from: "2022-10-31", to: "2022-12-31" }} + ${"notFuture"} | ${true} | ${{ from: "2023-01-01", to: "2023-01-02" }} | ${{ from: "2022-12-31", to: "2023-01-01" }} + ${"notPast"} | ${true} | ${{ from: "2022-12-31", to: "2023-01-01" }} | ${{ from: "2023-01-01", to: "2023-01-02" }} + ${"minDate"} | ${"2023-01-02"} | ${{ from: "2021-01-01", to: "2023-01-01" }} | ${{ from: "2023-01-02", to: "2023-01-08" }} + ${"maxDate"} | ${"2023-01-02"} | ${{ from: "2023-01-03", to: "2024-01-03" }} | ${{ from: "2022-01-02", to: "2023-01-02" }} + ${"excludedDates"} | ${["2023-01-02"]} | ${{ from: "2023-01-01", to: "2023-01-02" }} | ${{ from: "2023-01-01", to: "2023-01-03" }} + ${"numberOfDays"} | ${10} | ${{ from: "2023-01-01", to: "2023-01-02" }} | ${{ from: "2023-01-01", to: "2023-01-10" }} + `("$rule", ({ rule, ruleValue, invalid, valid }) => { + it(`should apply $rule when condition is met`, () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + [CONDITION_FIELD_ID]: { + uiType: "text-field", + }, + field: { + uiType: "date-range-field", + validation: [ + { + when: { + [CONDITION_FIELD_ID]: { + is: [{ filled: true }], + then: [{ [rule]: ruleValue, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + ], + }, + }, + }, + }); + + expect( + TestHelper.getError(() => schema.validateSync({ [CONDITION_FIELD_ID]: "hello", field: invalid })) + .message + ).toBe(ERROR_MESSAGE); + expect(() => schema.validateSync({ [CONDITION_FIELD_ID]: "hello", field: valid })).not.toThrowError(); + }); + + it(`should not apply $rule when condition is not met`, () => { + const schema = jsonToSchema({ + section: { + uiType: "section", + children: { + [CONDITION_FIELD_ID]: { + uiType: "text-field", + }, + field: { + uiType: "date-range-field", + validation: [ + { + when: { + [CONDITION_FIELD_ID]: { + is: [{ filled: true }], + then: [{ [rule]: ruleValue, errorMessage: ERROR_MESSAGE }], + }, + }, + }, + ], + }, + }, + }, + }); + + expect(() => schema.validateSync({ [CONDITION_FIELD_ID]: "", field: invalid })).not.toThrowError(); + }); + }); + }); + describe.each` rule | ruleValue | valid | invalid | errorMessage ${"future"} | ${true} | ${{ from: "2023-01-02", to: "2023-01-03" }} | ${{ from: "2021-01-01", to: "2022-01-01" }} | ${ERROR_MESSAGES.DATE_RANGE.MUST_BE_FUTURE} diff --git a/src/fields/date-range-field/date-range-field.ts b/src/fields/date-range-field/date-range-field.ts index 6fec64b..98d6d0a 100644 --- a/src/fields/date-range-field/date-range-field.ts +++ b/src/fields/date-range-field/date-range-field.ts @@ -1,15 +1,18 @@ import { DateTimeFormatter, LocalDate, ResolverStyle } from "@js-joda/core"; import { Locale } from "@js-joda/locale_en-us"; +import isEmpty from "lodash/isEmpty"; import * as Yup from "yup"; +import { IValidationRule } from "../../schema-generator"; import { ERROR_MESSAGES } from "../../shared"; import { DateTimeHelper } from "../../utils"; import { IFieldGenerator } from "../types"; -import { IDateRangeFieldSchema } from "./types"; +import { IDateRangeFieldSchema, IDateRangeInputValidationRule } from "./types"; -const isEmptyValue = (value: { from: string; to: string }) => !value || !value.from || !value.to; +const isEmptyValue = (value: { from: string | undefined; to: string | undefined }) => + !value || !value.from || !value.to; -const isValidDate = (value: string, formatter: DateTimeFormatter): boolean => { - if (!value || value === ERROR_MESSAGES.DATE.INVALID) return false; +const isValidDate = (value: string | undefined, formatter: DateTimeFormatter): boolean => { + if (!value || value === ERROR_MESSAGES.DATE_RANGE.INVALID) return false; try { LocalDate.parse(value, formatter); return true; @@ -18,6 +21,18 @@ const isValidDate = (value: string, formatter: DateTimeFormatter): boolean => { } }; +const getAppliedRule = ( + metaRules: (IValidationRule | IDateRangeInputValidationRule)[], + validation: IDateRangeFieldSchema["validation"], + key: string +): (IValidationRule & IDateRangeInputValidationRule) | undefined => { + const metaRule = metaRules?.find((rule) => rule && key in rule); + const validationRule = validation?.find((rule) => !!rule && key in rule); + if (!isEmpty(metaRule)) return metaRule as IValidationRule & IDateRangeInputValidationRule; + if (!isEmpty(validationRule)) return validationRule as IValidationRule & IDateRangeInputValidationRule; + return undefined; +}; + export const dateRangeField: IFieldGenerator = ( id, { dateFormat = "uuuu-MM-dd", validation, variant } @@ -25,18 +40,6 @@ export const dateRangeField: IFieldGenerator = ( const dateFormatter = DateTimeFormatter.ofPattern(dateFormat) .withResolverStyle(ResolverStyle.STRICT) .withLocale(Locale.ENGLISH); - const futureRule = validation?.find((rule) => "future" in rule); - const pastRule = validation?.find((rule) => "past" in rule); - const notFutureRule = validation?.find((rule) => "notFuture" in rule); - const notPastRule = validation?.find((rule) => "notPast" in rule); - const minDateRule = validation?.find((rule) => "minDate" in rule); - const maxDateRule = validation?.find((rule) => "maxDate" in rule); - const isRequiredRule = validation?.find((rule) => "required" in rule); - const excludedDatesRule = validation?.find((rule) => "excludedDates" in rule); - const noOfDaysRule = validation?.find((rule) => "numberOfDays" in rule); - - const minDate = DateTimeHelper.toLocalDateOrTime(minDateRule?.["minDate"], dateFormat, "date"); - const maxDate = DateTimeHelper.toLocalDateOrTime(maxDateRule?.["maxDate"], dateFormat, "date"); return { [id]: { @@ -45,53 +48,101 @@ export const dateRangeField: IFieldGenerator = ( from: Yup.string(), to: Yup.string(), }) - .test( - "is-empty-string", - isRequiredRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.REQUIRED, - (value) => { + .test({ + name: "is-empty-string", + test(value, context) { + const isRequiredRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "required" + ); if (!value || !isRequiredRule || !isRequiredRule.required) return true; - return !isEmptyValue(value); - } - ) - .test("is-date", ERROR_MESSAGES.DATE_RANGE.INVALID, (value) => { - if (isEmptyValue(value)) return true; - if (!isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter)) return false; - return ( - !!DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date") || - !!DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date") - ); + if (!isEmptyValue(value)) return true; + return this.createError({ + message: isRequiredRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.REQUIRED, + }); + }, }) - .test("future", futureRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.MUST_BE_FUTURE, (value) => { - if ( - isEmptyValue(value) || - !isValidDate(value.from, dateFormatter) || - !isValidDate(value.to, dateFormatter) || - !futureRule?.["future"] - ) - return true; - if (variant === "week") return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !!localDateFrom?.isAfter(LocalDate.now()) && !!localDateTo?.isAfter(LocalDate.now()); + .test({ + name: "is-date", + test(value) { + const dateFormatRule = validation?.find((rule) => !!rule && "dateFormat" in rule); + if (isEmptyValue(value)) return true; + if (!isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter)) { + return this.createError({ + message: dateFormatRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.INVALID, + }); + } + const isValid = + !!DateTimeHelper.toLocalDateOrTime(value.from as string, dateFormat, "date") || + !!DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + if (isValid) return true; + return this.createError({ + message: dateFormatRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.INVALID, + }); + }, }) - .test("past", pastRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.MUST_BE_PAST, (value) => { - if ( - isEmptyValue(value) || - !isValidDate(value.from, dateFormatter) || - !isValidDate(value.to, dateFormatter) || - !pastRule?.["past"] - ) - return true; - if (variant === "week") return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !!localDateFrom?.isBefore(LocalDate.now()) && !!localDateTo?.isBefore(LocalDate.now()); + .test({ + name: "future", + test(value, context) { + if (variant === "week") return true; + const futureRule = getAppliedRule(context.schema.describe().meta?.rules, validation, "future"); + if ( + isEmptyValue(value) || + !isValidDate(value.from, dateFormatter) || + !isValidDate(value.to, dateFormatter) || + !futureRule?.["future"] + ) + return true; + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !!localDateFrom?.isAfter(LocalDate.now()) && !!localDateTo?.isAfter(LocalDate.now()); + if (isValid) return true; + return this.createError({ + message: futureRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.MUST_BE_FUTURE, + }); + }, + }) + .test({ + name: "past", + test(value, context) { + if (variant === "week") return true; + const pastRule = getAppliedRule(context.schema.describe().meta?.rules, validation, "past"); + if ( + isEmptyValue(value) || + !isValidDate(value.from, dateFormatter) || + !isValidDate(value.to, dateFormatter) || + !pastRule?.["past"] + ) + return true; + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !!localDateFrom?.isBefore(LocalDate.now()) && !!localDateTo?.isBefore(LocalDate.now()); + if (isValid) return true; + return this.createError({ + message: pastRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.MUST_BE_PAST, + }); + }, }) - .test( - "not-future", - notFutureRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.CANNOT_BE_FUTURE, - (value) => { + .test({ + name: "not-future", + test(value, context) { if (variant === "week") return true; + const notFutureRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "notFuture" + ); if ( isEmptyValue(value) || !isValidDate(value.from, dateFormatter) || @@ -99,110 +150,208 @@ export const dateRangeField: IFieldGenerator = ( !notFutureRule?.["notFuture"] ) return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !localDateFrom?.isAfter(LocalDate.now()) && !localDateTo?.isAfter(LocalDate.now()); - } - ) - .test("not-past", notPastRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.CANNOT_BE_PAST, (value) => { - if (variant === "week") return true; - if ( - isEmptyValue(value) || - !isValidDate(value.from, dateFormatter) || - !isValidDate(value.to, dateFormatter) || - !notPastRule?.["notPast"] - ) - return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !localDateFrom?.isBefore(LocalDate.now()) && !localDateTo?.isBefore(LocalDate.now()); + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !localDateFrom?.isAfter(LocalDate.now()) && !localDateTo?.isAfter(LocalDate.now()); + if (isValid) return true; + return this.createError({ + message: notFutureRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.CANNOT_BE_FUTURE, + }); + }, }) - .test( - "min-date", - minDateRule?.errorMessage || - ERROR_MESSAGES.DATE_RANGE.MIN_DATE( - DateTimeHelper.formatDateTime(minDateRule?.["minDate"], "dd/MM/uuuu", "date") - ), - (value) => { + .test({ + name: "not-past", + test(value, context) { + if (variant === "week") return true; + const notPastRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "notPast" + ); if ( isEmptyValue(value) || !isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter) || - !minDate + !notPastRule?.["notPast"] ) return true; + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !localDateFrom?.isBefore(LocalDate.now()) && !localDateTo?.isBefore(LocalDate.now()); + if (isValid) return true; + return this.createError({ + message: notPastRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.CANNOT_BE_PAST, + }); + }, + }) + .test({ + name: "min-date", + test(value, context) { if (variant === "week") return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !localDateFrom?.isBefore(minDate) && !localDateTo?.isBefore(minDate); - } - ) - .test( - "max-date", - maxDateRule?.errorMessage || - ERROR_MESSAGES.DATE_RANGE.MAX_DATE( - DateTimeHelper.formatDateTime(maxDateRule?.["maxDate"], "dd/MM/uuuu", "date") - ), - (value) => { + const minDateRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "minDate" + ); + const effectiveMinDateStr = minDateRule?.["minDate"] as string | undefined; if ( isEmptyValue(value) || !isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter) || - !maxDate + !effectiveMinDateStr ) return true; + const effectiveMinDate = DateTimeHelper.toLocalDateOrTime( + effectiveMinDateStr, + dateFormat, + "date" + ); + if (!effectiveMinDate) return true; + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !localDateFrom?.isBefore(effectiveMinDate) && !localDateTo?.isBefore(effectiveMinDate); + if (isValid) return true; + return this.createError({ + message: + minDateRule?.errorMessage || + ERROR_MESSAGES.DATE_RANGE.MIN_DATE( + DateTimeHelper.formatDateTime(effectiveMinDateStr, "dd/MM/uuuu", "date") + ), + }); + }, + }) + .test({ + name: "max-date", + test(value, context) { if (variant === "week") return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return !localDateFrom?.isAfter(maxDate) && !localDateTo?.isAfter(maxDate); - } - ) - .test( - "excluded-dates", - excludedDatesRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.DISABLED_DATES, - (value) => { + const maxDateRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "maxDate" + ); + const effectiveMaxDateStr = maxDateRule?.["maxDate"] as string | undefined; + if ( + isEmptyValue(value) || + !isValidDate(value.from, dateFormatter) || + !isValidDate(value.to, dateFormatter) || + !effectiveMaxDateStr + ) + return true; + const effectiveMaxDate = DateTimeHelper.toLocalDateOrTime( + effectiveMaxDateStr, + dateFormat, + "date" + ); + if (!effectiveMaxDate) return true; + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = + !localDateFrom?.isAfter(effectiveMaxDate) && !localDateTo?.isAfter(effectiveMaxDate); + if (isValid) return true; + return this.createError({ + message: + maxDateRule?.errorMessage || + ERROR_MESSAGES.DATE_RANGE.MAX_DATE( + DateTimeHelper.formatDateTime(effectiveMaxDateStr, "dd/MM/uuuu", "date") + ), + }); + }, + }) + .test({ + name: "excluded-dates", + test(value, context) { if (variant === "week") return true; + const excludedDatesRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "excludedDates" + ); + const effectiveExcludedDates = excludedDatesRule?.["excludedDates"] as string[] | undefined; if ( isEmptyValue(value) || !isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter) || - !excludedDatesRule + !effectiveExcludedDates ) return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); try { - const mappedexcludedDates = excludedDatesRule["excludedDates"].map((date) => + const mappedexcludedDates = effectiveExcludedDates.map((date) => DateTimeHelper.toLocalDateOrTime(date, dateFormat, "date") ); for (const excludedDate of mappedexcludedDates) { - if (localDateFrom.isEqual(excludedDate) || localDateTo.isEqual(excludedDate)) - return false; + if ( + localDateFrom?.isEqual(excludedDate as LocalDate) || + localDateTo?.isEqual(excludedDate as LocalDate) + ) { + return this.createError({ + message: + excludedDatesRule?.errorMessage || ERROR_MESSAGES.DATE_RANGE.DISABLED_DATES, + }); + } } return true; } catch { return false; } - } - ) - .test( - "number-of-days", - noOfDaysRule?.errorMessage || - ERROR_MESSAGES.DATE_RANGE.MUST_HAVE_NUMBER_OF_DAYS(noOfDaysRule?.["numberOfDays"]), - (value) => { + }, + }) + .test({ + name: "number-of-days", + test(value, context) { if (variant === "week") return true; + const noOfDaysRule = getAppliedRule( + context.schema.describe().meta?.rules, + validation, + "numberOfDays" + ); + const effectiveNoOfDays = noOfDaysRule?.["numberOfDays"] as number | undefined; if ( isEmptyValue(value) || !isValidDate(value.from, dateFormatter) || !isValidDate(value.to, dateFormatter) || - !noOfDaysRule?.["numberOfDays"] + !effectiveNoOfDays ) return true; - const localDateFrom = DateTimeHelper.toLocalDateOrTime(value.from, dateFormat, "date"); - const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to, dateFormat, "date"); - return localDateTo.equals(localDateFrom.plusDays(noOfDaysRule?.["numberOfDays"] - 1)); - } - ), + const localDateFrom = DateTimeHelper.toLocalDateOrTime( + value.from as string, + dateFormat, + "date" + ); + const localDateTo = DateTimeHelper.toLocalDateOrTime(value.to as string, dateFormat, "date"); + const isValid = localDateTo?.equals(localDateFrom?.plusDays(effectiveNoOfDays - 1)); + if (isValid) return true; + return this.createError({ + message: + noOfDaysRule?.errorMessage || + ERROR_MESSAGES.DATE_RANGE.MUST_HAVE_NUMBER_OF_DAYS(effectiveNoOfDays), + }); + }, + }), validation, }, }; diff --git a/src/fields/date-range-field/types.ts b/src/fields/date-range-field/types.ts index bc40821..ac6e332 100644 --- a/src/fields/date-range-field/types.ts +++ b/src/fields/date-range-field/types.ts @@ -1,6 +1,7 @@ import { IFieldSchemaBase, IValidationRule } from "../../schema-generator"; export interface IDateRangeInputValidationRule extends IValidationRule { + dateFormat?: boolean | undefined; future?: boolean | undefined; past?: boolean | undefined; notFuture?: boolean | undefined; diff --git a/src/schema-generator/yup-helper.ts b/src/schema-generator/yup-helper.ts index 9dc6b76..799d8a1 100644 --- a/src/schema-generator/yup-helper.ts +++ b/src/schema-generator/yup-helper.ts @@ -100,10 +100,7 @@ export namespace YupHelper { { Object.keys(rule.when).forEach((fieldId) => { const isRule = rule.when[fieldId].is; - const thenRule = mapRules( - YupHelper.mapSchemaType(yupSchema.type as TYupSchemaType), - rule.when[fieldId].then - ); + const thenRule = mapRules(yupSchema.clone(), rule.when[fieldId].then); const otherwiseRule = rule.when[fieldId].otherwise && mapRules( @@ -148,6 +145,13 @@ export namespace YupHelper { console.error(`error applying "${customRuleKey}" condition to ${yupSchema.type} schema`); } } + + // record all conditions, regardless valid or not, to meta for reference in validation phase + Object.keys(rule).forEach((_) => { + yupSchema = yupSchema.meta({ + rules: [...(yupSchema.describe().meta?.["rules"] || []), rule], + }); + }); }); return yupSchema; diff --git a/src/shared/error-messages.ts b/src/shared/error-messages.ts index a9be528..3a1cdaa 100644 --- a/src/shared/error-messages.ts +++ b/src/shared/error-messages.ts @@ -41,12 +41,12 @@ export const ERROR_MESSAGES = { MUST_BE_PAST: "Dates must be in the past.", CANNOT_BE_FUTURE: "Dates cannot be in the future.", CANNOT_BE_PAST: "Dates cannot be in the past.", - MIN_DATE: (date: string) => `Dates cannot be earlier than ${date}`, - MAX_DATE: (date: string) => `Dates cannot be later than ${date}`, + MIN_DATE: (date: string) => `Dates cannot be earlier than ${date}.`, + MAX_DATE: (date: string) => `Dates cannot be later than ${date}.`, DISABLED_DATES: "Date range should not include disabled dates.", INVALID: "Invalid dates", REQUIRED: "Both dates are required", - MUST_HAVE_NUMBER_OF_DAYS: (numberOfDays: number) => `Selection should have ${numberOfDays} days`, + MUST_HAVE_NUMBER_OF_DAYS: (numberOfDays: number) => `Selection should have ${numberOfDays} days.`, }, TIME: { INVALID: "Invalid time",