mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-13 19:30:36 -05:00
Compare commits
16 Commits
fix-manage
...
chore/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d4a276558 | ||
|
|
777f748bde | ||
|
|
a48db4d127 | ||
|
|
7ffcfa5b36 | ||
|
|
1c527e92a1 | ||
|
|
1ed6e24a90 | ||
|
|
17bf8a96f7 | ||
|
|
02f4912c30 | ||
|
|
16fe95ee95 | ||
|
|
e91d6dc048 | ||
|
|
f403160563 | ||
|
|
a28c9e74af | ||
|
|
40a1bfb372 | ||
|
|
24b814d578 | ||
|
|
7121063ee9 | ||
|
|
66a9f06796 |
@@ -3,11 +3,12 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DEFAULT_DATE_STORAGE_FORMAT } from "@formbricks/types/surveys/date-formats";
|
||||||
import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||||
@@ -32,13 +33,8 @@ export const DateElementSummary = ({ elementSummary, environmentId, survey, loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderResponseValue = (value: string) => {
|
const renderResponseValue = (value: string) => {
|
||||||
const parsedDate = new Date(value);
|
const format = elementSummary.element?.format ?? DEFAULT_DATE_STORAGE_FORMAT;
|
||||||
|
return formatStoredDateForDisplay(value, format, `${t("common.invalid_date")}(${value})`);
|
||||||
const formattedDate = isNaN(parsedDate.getTime())
|
|
||||||
? `${t("common.invalid_date")}(${value})`
|
|
||||||
: formatDateWithOrdinal(parsedDate);
|
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
43
apps/web/lib/utils/date-display.test.ts
Normal file
43
apps/web/lib/utils/date-display.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { formatStoredDateForDisplay } from "./date-display";
|
||||||
|
|
||||||
|
describe("formatStoredDateForDisplay", () => {
|
||||||
|
test("returns formatted date for valid ISO string (y-M-d)", () => {
|
||||||
|
const result = formatStoredDateForDisplay("2024-03-13", "y-M-d", "fallback");
|
||||||
|
expect(result).toContain("2024");
|
||||||
|
expect(result).toContain("13");
|
||||||
|
expect(result).not.toBe("fallback");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns formatted date for valid d-M-y string", () => {
|
||||||
|
const result = formatStoredDateForDisplay("20-03-2026", "d-M-y", "fallback");
|
||||||
|
expect(result).toContain("2026");
|
||||||
|
expect(result).toContain("20");
|
||||||
|
expect(result).not.toBe("fallback");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns formatted date for valid M-d-y string", () => {
|
||||||
|
const result = formatStoredDateForDisplay("03-20-2026", "M-d-y", "fallback");
|
||||||
|
expect(result).toContain("2026");
|
||||||
|
expect(result).toContain("20");
|
||||||
|
expect(result).not.toBe("fallback");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns fallback when value is unparseable", () => {
|
||||||
|
const fallback = "Invalid date(bad)";
|
||||||
|
const result = formatStoredDateForDisplay("bad", "y-M-d", fallback);
|
||||||
|
expect(result).toBe(fallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns fallback when value is empty", () => {
|
||||||
|
const fallback = "—";
|
||||||
|
const result = formatStoredDateForDisplay("", "y-M-d", fallback);
|
||||||
|
expect(result).toBe(fallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns fallback when value is malformed (wrong format)", () => {
|
||||||
|
const fallback = "Invalid date(13-03-2024)";
|
||||||
|
const result = formatStoredDateForDisplay("13-03-2024", "y-M-d", fallback);
|
||||||
|
expect(result).toBe(fallback);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
apps/web/lib/utils/date-display.ts
Normal file
17
apps/web/lib/utils/date-display.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { TSurveyDateStorageFormat } from "@formbricks/surveys/date-format";
|
||||||
|
import { parseDateByFormat } from "@formbricks/surveys/date-format";
|
||||||
|
import { formatDateWithOrdinal } from "./datetime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a stored date string with the given format and returns a display string.
|
||||||
|
* If parsing fails, returns the provided fallback (e.g. raw value or "Invalid date(value)").
|
||||||
|
*/
|
||||||
|
export function formatStoredDateForDisplay(
|
||||||
|
value: string,
|
||||||
|
format: TSurveyDateStorageFormat,
|
||||||
|
fallback: string
|
||||||
|
): string {
|
||||||
|
const parsed = parseDateByFormat(value, format);
|
||||||
|
if (parsed === null) return fallback;
|
||||||
|
return formatDateWithOrdinal(parsed);
|
||||||
|
}
|
||||||
@@ -480,6 +480,36 @@ describe("recall utility functions", () => {
|
|||||||
expect(result).toBe("You joined on January 1st, 2023");
|
expect(result).toBe("You joined on January 1st, 2023");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("formats d-M-y date values in recall", () => {
|
||||||
|
const text = "Event on #recall:eventDate/fallback:none#";
|
||||||
|
const responseData: TResponseData = {
|
||||||
|
eventDate: "20-03-2026",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseRecallInfo(text, responseData);
|
||||||
|
expect(result).toBe("Event on January 1st, 2023");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formats M-d-y date values in recall", () => {
|
||||||
|
const text = "Due #recall:dueDate/fallback:none#";
|
||||||
|
const responseData: TResponseData = {
|
||||||
|
dueDate: "03-20-2026",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseRecallInfo(text, responseData);
|
||||||
|
expect(result).toBe("Due January 1st, 2023");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leaves non-date string unchanged in recall", () => {
|
||||||
|
const text = "Note: #recall:note/fallback:none#";
|
||||||
|
const responseData: TResponseData = {
|
||||||
|
note: "some text",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseRecallInfo(text, responseData);
|
||||||
|
expect(result).toBe("Note: some text");
|
||||||
|
});
|
||||||
|
|
||||||
test("formats array values as comma-separated list", () => {
|
test("formats array values as comma-separated list", () => {
|
||||||
const text = "Your selections: #recall:preferences/fallback:none#";
|
const text = "Your selections: #recall:preferences/fallback:none#";
|
||||||
const responseData: TResponseData = {
|
const responseData: TResponseData = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { parseDateWithFormats } from "@formbricks/surveys/date-format";
|
||||||
import { type TI18nString } from "@formbricks/types/i18n";
|
import { type TI18nString } from "@formbricks/types/i18n";
|
||||||
import { TResponseData, TResponseDataValue, TResponseVariables } from "@formbricks/types/responses";
|
import { TResponseData, TResponseDataValue, TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
@@ -6,7 +7,7 @@ import { getTextContent } from "@formbricks/types/surveys/validation";
|
|||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
import { formatDateWithOrdinal, isValidDateString } from "./datetime";
|
import { formatDateWithOrdinal } from "./datetime";
|
||||||
|
|
||||||
export interface fallbacks {
|
export interface fallbacks {
|
||||||
[id: string]: string;
|
[id: string]: string;
|
||||||
@@ -255,8 +256,11 @@ export const parseRecallInfo = (
|
|||||||
|
|
||||||
// Apply formatting for special value types
|
// Apply formatting for special value types
|
||||||
if (value) {
|
if (value) {
|
||||||
if (isValidDateString(value as string)) {
|
if (typeof value === "string") {
|
||||||
value = formatDateWithOrdinal(new Date(value as string));
|
const parsedDate = parseDateWithFormats(value);
|
||||||
|
if (parsedDate !== null) {
|
||||||
|
value = formatDateWithOrdinal(parsedDate);
|
||||||
|
}
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
value = value.filter((item) => item).join(", ");
|
value = value.filter((item) => item).join(", ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react";
|
import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
import { TResponseDataValue } from "@formbricks/types/responses";
|
import { TResponseDataValue } from "@formbricks/types/responses";
|
||||||
|
import { DEFAULT_DATE_STORAGE_FORMAT } from "@formbricks/types/surveys/date-formats";
|
||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||||
import { processResponseData } from "@/lib/responses";
|
import { processResponseData } from "@/lib/responses";
|
||||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||||
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
||||||
@@ -63,11 +65,15 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
|||||||
break;
|
break;
|
||||||
case TSurveyElementTypeEnum.Date:
|
case TSurveyElementTypeEnum.Date:
|
||||||
if (typeof responseData === "string") {
|
if (typeof responseData === "string") {
|
||||||
const parsedDate = new Date(responseData);
|
const format = element.format ?? DEFAULT_DATE_STORAGE_FORMAT;
|
||||||
|
const formatted = formatStoredDateForDisplay(responseData, format, responseData);
|
||||||
const formattedDate = isNaN(parsedDate.getTime()) ? responseData : formatDateWithOrdinal(parsedDate);
|
if (formatted === responseData) {
|
||||||
|
logger.warn(
|
||||||
return <p className="ph-no-capture my-1 truncate font-normal text-slate-700">{formattedDate}</p>;
|
{ elementId: element.id, format, value: responseData },
|
||||||
|
"[RenderResponse] could not parse date response value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p className="ph-no-capture my-1 truncate font-normal text-slate-700">{formatted}</p>;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TSurveyElementTypeEnum.PictureSelection:
|
case TSurveyElementTypeEnum.PictureSelection:
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX } from "react";
|
import { type JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
DATE_STORAGE_FORMATS,
|
||||||
|
DATE_STORAGE_FORMAT_IDS,
|
||||||
|
type TSurveyDateStorageFormat,
|
||||||
|
} from "@formbricks/types/surveys/date-formats";
|
||||||
import type { TSurveyDateElement, TSurveyElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyDateElement, TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
@@ -27,20 +32,10 @@ interface IDateElementFormProps {
|
|||||||
isExternalUrlsAllowed?: boolean;
|
isExternalUrlsAllowed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateOptions = [
|
const dateOptions = DATE_STORAGE_FORMAT_IDS.map((value) => ({
|
||||||
{
|
value,
|
||||||
value: "M-d-y",
|
label: DATE_STORAGE_FORMATS[value].label,
|
||||||
label: "MM-DD-YYYY",
|
}));
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "d-M-y",
|
|
||||||
label: "DD-MM-YYYY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "y-M-d",
|
|
||||||
label: "YYYY-MM-DD",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DateElementForm = ({
|
export const DateElementForm = ({
|
||||||
element,
|
element,
|
||||||
@@ -122,7 +117,7 @@ export const DateElementForm = ({
|
|||||||
options={dateOptions}
|
options={dateOptions}
|
||||||
currentOption={element.format}
|
currentOption={element.format}
|
||||||
handleOptionChange={(value: string) =>
|
handleOptionChange={(value: string) =>
|
||||||
updateElement(elementIdx, { format: value as "M-d-y" | "d-M-y" | "y-M-d" })
|
updateElement(elementIdx, { format: value as TSurveyDateStorageFormat })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formbricks/types": "workspace:*",
|
||||||
"@formkit/auto-animate": "0.9.0",
|
"@formkit/auto-animate": "0.9.0",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||||
|
|||||||
@@ -1,9 +1,45 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
DATE_STORAGE_FORMATS,
|
||||||
|
DEFAULT_DATE_STORAGE_FORMAT,
|
||||||
|
type TSurveyDateStorageFormat,
|
||||||
|
getOutputOrder,
|
||||||
|
} from "@formbricks/types/surveys/date-formats";
|
||||||
import { Calendar } from "@/components/general/calendar";
|
import { Calendar } from "@/components/general/calendar";
|
||||||
import { ElementError } from "@/components/general/element-error";
|
import { ElementError } from "@/components/general/element-error";
|
||||||
import { ElementHeader } from "@/components/general/element-header";
|
import { ElementHeader } from "@/components/general/element-header";
|
||||||
import { getDateFnsLocale } from "@/lib/locale";
|
import { getDateFnsLocale } from "@/lib/locale";
|
||||||
|
|
||||||
|
export type DateStorageFormat = TSurveyDateStorageFormat;
|
||||||
|
|
||||||
|
/** Optional whitespace + three hyphen-separated digit segments (validates shape and digits in one pass). */
|
||||||
|
const DATE_PARTS_REGEX = /^\s*(?<part1>\d+)-(?<part2>\d+)-(?<part3>\d+)\s*$/;
|
||||||
|
|
||||||
|
function parseValueToDate(value: string, format: DateStorageFormat): Date | undefined {
|
||||||
|
const match = DATE_PARTS_REGEX.exec(value);
|
||||||
|
if (!match?.groups) return undefined;
|
||||||
|
const { part1, part2, part3 } = match.groups;
|
||||||
|
const nums = [Number.parseInt(part1, 10), Number.parseInt(part2, 10), Number.parseInt(part3, 10)];
|
||||||
|
const effectiveFormat = part1.length === 4 ? DEFAULT_DATE_STORAGE_FORMAT : format;
|
||||||
|
const order = DATE_STORAGE_FORMATS[effectiveFormat].parseOrder;
|
||||||
|
|
||||||
|
const year = nums[order.yearIdx];
|
||||||
|
const month = nums[order.monthIdx];
|
||||||
|
const day = nums[order.dayIdx];
|
||||||
|
|
||||||
|
if (month < 1 || month > 12 || day < 1 || day > 31) return undefined;
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
||||||
|
return undefined;
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateForStorage(year: string, month: string, day: string, format: DateStorageFormat): string {
|
||||||
|
const comps = [year, month, day];
|
||||||
|
const [i, j, k] = getOutputOrder(DATE_STORAGE_FORMATS[format].parseOrder);
|
||||||
|
return `${comps[i]}-${comps[j]}-${comps[k]}`;
|
||||||
|
}
|
||||||
|
|
||||||
interface DateElementProps {
|
interface DateElementProps {
|
||||||
/** Unique identifier for the element container */
|
/** Unique identifier for the element container */
|
||||||
elementId: string;
|
elementId: string;
|
||||||
@@ -13,10 +49,12 @@ interface DateElementProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
/** Unique identifier for the date input */
|
/** Unique identifier for the date input */
|
||||||
inputId: string;
|
inputId: string;
|
||||||
/** Current date value in ISO format (YYYY-MM-DD) */
|
/** Current date value (format depends on outputFormat; legacy is often YYYY-MM-DD) */
|
||||||
value?: string;
|
value?: string;
|
||||||
/** Callback function called when the date value changes */
|
/** Callback function called when the date value changes */
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
/** Format for the value passed to onChange (default y-M-d = ISO) */
|
||||||
|
outputFormat?: DateStorageFormat;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
/** Custom label for the required indicator */
|
/** Custom label for the required indicator */
|
||||||
@@ -46,6 +84,7 @@ function DateElement({
|
|||||||
inputId,
|
inputId,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
outputFormat = DEFAULT_DATE_STORAGE_FORMAT,
|
||||||
required = false,
|
required = false,
|
||||||
requiredLabel,
|
requiredLabel,
|
||||||
minDate,
|
minDate,
|
||||||
@@ -57,42 +96,31 @@ function DateElement({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
videoUrl,
|
videoUrl,
|
||||||
}: Readonly<DateElementProps>): React.JSX.Element {
|
}: Readonly<DateElementProps>): React.JSX.Element {
|
||||||
// Initialize date from value string, parsing as local time to avoid timezone issues
|
|
||||||
const [date, setDate] = React.useState<Date | undefined>(() => {
|
const [date, setDate] = React.useState<Date | undefined>(() => {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
// Parse YYYY-MM-DD format as local date (not UTC)
|
return parseValueToDate(value, outputFormat);
|
||||||
const [year, month, day] = value.split("-").map(Number);
|
|
||||||
return new Date(year, month - 1, day);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync date state when value prop changes
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (value) {
|
if (!value) {
|
||||||
// Parse YYYY-MM-DD format as local date (not UTC)
|
|
||||||
const [year, month, day] = value.split("-").map(Number);
|
|
||||||
const newDate = new Date(year, month - 1, day);
|
|
||||||
setDate((prevDate) => {
|
|
||||||
// Only update if the date actually changed to avoid unnecessary re-renders
|
|
||||||
if (!prevDate || newDate.getTime() !== prevDate.getTime()) {
|
|
||||||
return newDate;
|
|
||||||
}
|
|
||||||
return prevDate;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setDate(undefined);
|
setDate(undefined);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [value]);
|
const newDate = parseValueToDate(value, outputFormat);
|
||||||
|
setDate((prevDate) => {
|
||||||
|
if (!newDate) return undefined;
|
||||||
|
if (prevDate?.getTime() !== newDate.getTime()) return newDate;
|
||||||
|
return prevDate;
|
||||||
|
});
|
||||||
|
}, [value, outputFormat]);
|
||||||
|
|
||||||
// Convert Date to ISO string (YYYY-MM-DD) when date changes
|
|
||||||
const handleDateSelect = (selectedDate: Date | undefined): void => {
|
const handleDateSelect = (selectedDate: Date | undefined): void => {
|
||||||
setDate(selectedDate);
|
setDate(selectedDate);
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
// Convert to ISO format (YYYY-MM-DD) using local time to avoid timezone issues
|
|
||||||
const year = String(selectedDate.getFullYear());
|
const year = String(selectedDate.getFullYear());
|
||||||
const month = String(selectedDate.getMonth() + 1).padStart(2, "0");
|
const month = String(selectedDate.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(selectedDate.getDate()).padStart(2, "0");
|
const day = String(selectedDate.getDate()).padStart(2, "0");
|
||||||
const isoString = `${year}-${month}-${day}`;
|
onChange(formatDateForStorage(year, month, day, outputFormat));
|
||||||
onChange(isoString);
|
|
||||||
} else {
|
} else {
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
"./validation": {
|
"./validation": {
|
||||||
"types": "./dist/validation.d.ts",
|
"types": "./dist/validation.d.ts",
|
||||||
"import": "./dist/validation.js"
|
"import": "./dist/validation.js"
|
||||||
|
},
|
||||||
|
"./date-format": {
|
||||||
|
"types": "./dist/date-format.d.ts",
|
||||||
|
"import": "./dist/date-format.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from "preact/hooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateElement as SurveyUIDateElement } from "@formbricks/survey-ui";
|
import { DateElement as SurveyUIDateElement } from "@formbricks/survey-ui";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
|
import { DEFAULT_DATE_STORAGE_FORMAT } from "@formbricks/types/surveys/date-formats";
|
||||||
import type { TSurveyDateElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyDateElement } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
@@ -68,6 +69,7 @@ export function DateElement({
|
|||||||
description={element.subheader ? getLocalizedValue(element.subheader, languageCode) : undefined}
|
description={element.subheader ? getLocalizedValue(element.subheader, languageCode) : undefined}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
outputFormat={element.format ?? DEFAULT_DATE_STORAGE_FORMAT}
|
||||||
minDate={getMinDate()}
|
minDate={getMinDate()}
|
||||||
maxDate={getMaxDate()}
|
maxDate={getMaxDate()}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
|
|||||||
1
packages/surveys/src/date-format.ts
Normal file
1
packages/surveys/src/date-format.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { parseDateByFormat, parseDateWithFormats, type TSurveyDateStorageFormat } from "./lib/date-format";
|
||||||
131
packages/surveys/src/lib/date-format.test.ts
Normal file
131
packages/surveys/src/lib/date-format.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { parseDateByFormat, parseDateWithFormats } from "./date-format";
|
||||||
|
|
||||||
|
describe("parseDateByFormat", () => {
|
||||||
|
test("parses ISO (y-M-d) string with default format", () => {
|
||||||
|
const result = parseDateByFormat("2024-03-13");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses ISO string with explicit y-M-d format", () => {
|
||||||
|
const result = parseDateByFormat("2024-01-05", "y-M-d");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(0);
|
||||||
|
expect(result!.getDate()).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses d-M-y string (DD-MM-YYYY)", () => {
|
||||||
|
const result = parseDateByFormat("13-03-2024", "d-M-y");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses M-d-y string (MM-DD-YYYY)", () => {
|
||||||
|
const result = parseDateByFormat("03-13-2024", "M-d-y");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("backward compat: value starting with 4 digits (YYYY) parses as ISO regardless of format", () => {
|
||||||
|
const value = "2024-03-13";
|
||||||
|
expect(parseDateByFormat(value, "d-M-y")).not.toBeNull();
|
||||||
|
expect(parseDateByFormat(value, "d-M-y")!.getDate()).toBe(13);
|
||||||
|
expect(parseDateByFormat(value, "M-d-y")).not.toBeNull();
|
||||||
|
expect(parseDateByFormat(value, "M-d-y")!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("backward compat: single-digit month/day in ISO-style parses", () => {
|
||||||
|
const result = parseDateByFormat("2024-3-5");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for empty string", () => {
|
||||||
|
expect(parseDateByFormat("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for malformed string (not three parts)", () => {
|
||||||
|
expect(parseDateByFormat("2024-03")).toBeNull();
|
||||||
|
expect(parseDateByFormat("2024")).toBeNull();
|
||||||
|
expect(parseDateByFormat("a-b-c")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for invalid numbers (e.g. month 13)", () => {
|
||||||
|
expect(parseDateByFormat("2024-13-01")).toBeNull();
|
||||||
|
expect(parseDateByFormat("2024-00-01")).toBeNull();
|
||||||
|
expect(parseDateByFormat("32-03-2024", "d-M-y")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for invalid date (e.g. Feb 30)", () => {
|
||||||
|
expect(parseDateByFormat("2024-02-30")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace", () => {
|
||||||
|
const result = parseDateByFormat(" 2024-03-13 ");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leap year parses correctly", () => {
|
||||||
|
const result = parseDateByFormat("2024-02-29");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getMonth()).toBe(1);
|
||||||
|
expect(result!.getDate()).toBe(29);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("single-digit month/day in d-M-y format", () => {
|
||||||
|
const result = parseDateByFormat("5-3-2024", "d-M-y");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("single-digit month/day in M-d-y format", () => {
|
||||||
|
const result = parseDateByFormat("3-5-2024", "M-d-y");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseDateWithFormats", () => {
|
||||||
|
test("parses ISO string (y-M-d)", () => {
|
||||||
|
const result = parseDateWithFormats("2024-03-13");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2024);
|
||||||
|
expect(result!.getDate()).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses d-M-y string when format unknown", () => {
|
||||||
|
const result = parseDateWithFormats("20-03-2026");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2026);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses M-d-y string when format unknown", () => {
|
||||||
|
const result = parseDateWithFormats("03-20-2026");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.getFullYear()).toBe(2026);
|
||||||
|
expect(result!.getMonth()).toBe(2);
|
||||||
|
expect(result!.getDate()).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for unparseable string", () => {
|
||||||
|
expect(parseDateWithFormats("not-a-date")).toBeNull();
|
||||||
|
expect(parseDateWithFormats("")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
77
packages/surveys/src/lib/date-format.ts
Normal file
77
packages/surveys/src/lib/date-format.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
DATE_STORAGE_FORMATS,
|
||||||
|
DATE_STORAGE_FORMATS_LIST,
|
||||||
|
DEFAULT_DATE_STORAGE_FORMAT,
|
||||||
|
type TSurveyDateStorageFormat,
|
||||||
|
} from "@formbricks/types/surveys/date-formats";
|
||||||
|
|
||||||
|
export type { TSurveyDateStorageFormat } from "@formbricks/types/surveys/date-formats";
|
||||||
|
|
||||||
|
const ISO_FIRST_CHARS = /^\d{4}/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string stored in response data using the element's storage format.
|
||||||
|
* Uses the format registry from @formbricks/types for data-driven parsing.
|
||||||
|
*
|
||||||
|
* Backward compatibility: if the value starts with 4 digits (YYYY), it is treated
|
||||||
|
* as ISO (y-M-d) regardless of format, so legacy YYYY-MM-DD values parse correctly.
|
||||||
|
*
|
||||||
|
* @param value - The stored date string (e.g. "2024-03-13", "13-03-2024", "03-13-2024")
|
||||||
|
* @param format - The format used when the value was stored; defaults to "y-M-d" (ISO)
|
||||||
|
* @returns Parsed Date in local time, or null if invalid
|
||||||
|
*/
|
||||||
|
export function parseDateByFormat(
|
||||||
|
value: string,
|
||||||
|
format: TSurveyDateStorageFormat = DEFAULT_DATE_STORAGE_FORMAT
|
||||||
|
): Date | null {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed || typeof trimmed !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = trimmed.split("-");
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useIso = ISO_FIRST_CHARS.test(trimmed);
|
||||||
|
const effectiveFormat = useIso ? DEFAULT_DATE_STORAGE_FORMAT : format;
|
||||||
|
|
||||||
|
const order = DATE_STORAGE_FORMATS[effectiveFormat].parseOrder;
|
||||||
|
const nums = parts.map((p) => Number.parseInt(p, 10));
|
||||||
|
if (nums.some(Number.isNaN)) return null;
|
||||||
|
const year = nums[order.yearIdx];
|
||||||
|
const month = nums[order.monthIdx];
|
||||||
|
const day = nums[order.dayIdx];
|
||||||
|
|
||||||
|
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to parse a date string using each known storage format in order.
|
||||||
|
* Use when the storage format is unknown (e.g. recall placeholders).
|
||||||
|
*
|
||||||
|
* Ambiguity: Values like "03-05-2024" can be March 5th (M-d-y) or May 3rd (d-M-y).
|
||||||
|
* Formats are tried in DATE_STORAGE_FORMATS_LIST order (M-d-y, then d-M-y, then y-M-d);
|
||||||
|
* the first successful parse is returned. M-d-y is preferred when format metadata is
|
||||||
|
* unavailable so that common US-style input is accepted first.
|
||||||
|
*
|
||||||
|
* @param value - Stored date string
|
||||||
|
* @returns Parsed Date or null if no format matched
|
||||||
|
*/
|
||||||
|
export function parseDateWithFormats(value: string): Date | null {
|
||||||
|
for (const format of DATE_STORAGE_FORMATS_LIST) {
|
||||||
|
const parsed = parseDateByFormat(value, format);
|
||||||
|
if (parsed !== null) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -807,6 +807,57 @@ describe("Survey Logic", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("date equals with d-M-y stored value", () => {
|
||||||
|
const dataWithDMy: TResponseData = { ...mockData, q5: "01-01-2023" };
|
||||||
|
const condition: TConditionGroup = {
|
||||||
|
id: "g1",
|
||||||
|
connector: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
id: "c1",
|
||||||
|
operator: "equals",
|
||||||
|
leftOperand: { type: "element", value: "q5" },
|
||||||
|
rightOperand: { type: "static", value: "2023-01-01" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(evaluateLogic(mockSurvey, dataWithDMy, mockVariablesData, condition, "default")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("date comparison returns false when response date is unparseable", () => {
|
||||||
|
const dataInvalidDate: TResponseData = { ...mockData, q5: "not-a-date" };
|
||||||
|
const condition: TConditionGroup = {
|
||||||
|
id: "g1",
|
||||||
|
connector: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
id: "c1",
|
||||||
|
operator: "equals",
|
||||||
|
leftOperand: { type: "element", value: "q5" },
|
||||||
|
rightOperand: { type: "static", value: "2023-01-01" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(evaluateLogic(mockSurvey, dataInvalidDate, mockVariablesData, condition, "default")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isAfter returns false when left date is unparseable", () => {
|
||||||
|
const dataInvalidDate: TResponseData = { ...mockData, q5: "invalid" };
|
||||||
|
const condition: TConditionGroup = {
|
||||||
|
id: "g1",
|
||||||
|
connector: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
id: "c1",
|
||||||
|
operator: "isAfter",
|
||||||
|
leftOperand: { type: "element", value: "q5" },
|
||||||
|
rightOperand: { type: "static", value: "2022-01-01" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(evaluateLogic(mockSurvey, dataInvalidDate, mockVariablesData, condition, "default")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("evaluates array inclusion operators", () => {
|
test("evaluates array inclusion operators", () => {
|
||||||
// Tests for includesAllOf, includesOneOf, etc.
|
// Tests for includesAllOf, includesOneOf, etc.
|
||||||
const includesAllOfCondition: TConditionGroup = {
|
const includesAllOfCondition: TConditionGroup = {
|
||||||
|
|||||||
@@ -2,12 +2,56 @@ import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
|||||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { type TActionCalculate, type TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
import { type TActionCalculate, type TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
||||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
|
import { DEFAULT_DATE_STORAGE_FORMAT } from "@formbricks/types/surveys/date-formats";
|
||||||
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
|
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||||
import { type TSurveyVariable } from "@formbricks/types/surveys/types";
|
import { type TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||||
|
import { parseDateByFormat } from "@/lib/date-format";
|
||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
import { getElementsFromSurveyBlocks } from "./utils";
|
import { getElementsFromSurveyBlocks } from "./utils";
|
||||||
|
|
||||||
|
/** Coerce to string for date comparison; avoids Object's default stringification. */
|
||||||
|
function toDateOperandString(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "number") return String(value);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateOperand(value: string, field: TSurveyElement | ""): Date | null {
|
||||||
|
const format =
|
||||||
|
field && typeof field === "object" && field.type === TSurveyElementTypeEnum.Date && "format" in field
|
||||||
|
? (field.format ?? DEFAULT_DATE_STORAGE_FORMAT)
|
||||||
|
: DEFAULT_DATE_STORAGE_FORMAT;
|
||||||
|
return parseDateByFormat(value, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareDateOperands(
|
||||||
|
leftValue: string,
|
||||||
|
rightValue: string,
|
||||||
|
leftField: TSurveyElement,
|
||||||
|
rightField: TSurveyElement,
|
||||||
|
operator: string,
|
||||||
|
compare: (left: Date, right: Date) => boolean
|
||||||
|
): boolean {
|
||||||
|
const leftDate = parseDateOperand(leftValue, leftField);
|
||||||
|
const rightDate = parseDateOperand(rightValue, rightField);
|
||||||
|
if (leftDate === null) {
|
||||||
|
console.warn(`[logic] ${operator}: could not parse left date`, {
|
||||||
|
elementId: leftField.id,
|
||||||
|
value: leftValue,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rightDate === null) {
|
||||||
|
console.warn(`[logic] ${operator}: could not parse right date`, {
|
||||||
|
elementId: rightField.id,
|
||||||
|
value: rightValue,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return compare(leftDate, rightDate);
|
||||||
|
}
|
||||||
|
|
||||||
const getVariableValue = (
|
const getVariableValue = (
|
||||||
variables: TSurveyVariable[],
|
variables: TSurveyVariable[],
|
||||||
variableId: string,
|
variableId: string,
|
||||||
@@ -265,12 +309,17 @@ const evaluateSingleCondition = (
|
|||||||
typeof leftValue === "string" &&
|
typeof leftValue === "string" &&
|
||||||
typeof rightValue === "string"
|
typeof rightValue === "string"
|
||||||
) {
|
) {
|
||||||
// when left value is of date question and right value is string
|
return compareDateOperands(
|
||||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
leftValue,
|
||||||
|
String(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"equals",
|
||||||
|
(l, r) => l.getTime() === r.getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
|
||||||
if (condition.rightOperand?.type === "element") {
|
if (condition.rightOperand?.type === "element") {
|
||||||
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||||
@@ -281,7 +330,14 @@ const evaluateSingleCondition = (
|
|||||||
typeof leftValue === "string" &&
|
typeof leftValue === "string" &&
|
||||||
typeof rightValue === "string"
|
typeof rightValue === "string"
|
||||||
) {
|
) {
|
||||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
return compareDateOperands(
|
||||||
|
leftValue,
|
||||||
|
String(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"equals",
|
||||||
|
(l, r) => l.getTime() === r.getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,17 +360,22 @@ const evaluateSingleCondition = (
|
|||||||
return !leftValue.includes(rightValue);
|
return !leftValue.includes(rightValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// when left value is of date question and right value is string
|
|
||||||
if (
|
if (
|
||||||
condition.leftOperand.type === "element" &&
|
condition.leftOperand.type === "element" &&
|
||||||
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
||||||
typeof leftValue === "string" &&
|
typeof leftValue === "string" &&
|
||||||
typeof rightValue === "string"
|
typeof rightValue === "string"
|
||||||
) {
|
) {
|
||||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
return compareDateOperands(
|
||||||
|
leftValue,
|
||||||
|
String(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"doesNotEqual",
|
||||||
|
(l, r) => l.getTime() !== r.getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
|
||||||
if (condition.rightOperand?.type === "element") {
|
if (condition.rightOperand?.type === "element") {
|
||||||
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||||
@@ -325,7 +386,14 @@ const evaluateSingleCondition = (
|
|||||||
typeof leftValue === "string" &&
|
typeof leftValue === "string" &&
|
||||||
typeof rightValue === "string"
|
typeof rightValue === "string"
|
||||||
) {
|
) {
|
||||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
return compareDateOperands(
|
||||||
|
leftValue,
|
||||||
|
String(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"doesNotEqual",
|
||||||
|
(l, r) => l.getTime() !== r.getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,9 +481,23 @@ const evaluateSingleCondition = (
|
|||||||
case "isNotClicked":
|
case "isNotClicked":
|
||||||
return leftValue !== "clicked";
|
return leftValue !== "clicked";
|
||||||
case "isAfter":
|
case "isAfter":
|
||||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
return compareDateOperands(
|
||||||
|
toDateOperandString(leftValue),
|
||||||
|
toDateOperandString(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"isAfter",
|
||||||
|
(l, r) => l.getTime() > r.getTime()
|
||||||
|
);
|
||||||
case "isBefore":
|
case "isBefore":
|
||||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
return compareDateOperands(
|
||||||
|
toDateOperandString(leftValue),
|
||||||
|
toDateOperandString(rightValue),
|
||||||
|
leftField as TSurveyElement,
|
||||||
|
rightField as TSurveyElement,
|
||||||
|
"isBefore",
|
||||||
|
(l, r) => l.getTime() < r.getTime()
|
||||||
|
);
|
||||||
case "isBooked":
|
case "isBooked":
|
||||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||||
case "isPartiallySubmitted":
|
case "isPartiallySubmitted":
|
||||||
|
|||||||
@@ -585,6 +585,58 @@ describe("validators", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("date validators with element format and parse failure", () => {
|
||||||
|
const dateElementDMy: TSurveyElement = {
|
||||||
|
id: "date1",
|
||||||
|
type: TSurveyElementTypeEnum.Date,
|
||||||
|
headline: { default: "Date" },
|
||||||
|
required: false,
|
||||||
|
format: "d-M-y",
|
||||||
|
} as TSurveyElement;
|
||||||
|
|
||||||
|
test("isLaterThan uses element format d-M-y", () => {
|
||||||
|
const result = validators.isLaterThan.check("20-03-2026", { date: "2024-01-01" }, dateElementDMy);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isEarlierThan uses element format d-M-y", () => {
|
||||||
|
const result = validators.isEarlierThan.check("01-01-2024", { date: "2024-12-31" }, dateElementDMy);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isBetween with d-M-y value", () => {
|
||||||
|
const result = validators.isBetween.check(
|
||||||
|
"15-06-2024",
|
||||||
|
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||||
|
dateElementDMy
|
||||||
|
);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isLaterThan returns valid false when response date is unparseable", () => {
|
||||||
|
const result = validators.isLaterThan.check("not-a-date", { date: "2024-01-01" }, dateElementDMy);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isBetween returns valid false when response date is unparseable", () => {
|
||||||
|
const result = validators.isBetween.check(
|
||||||
|
"invalid",
|
||||||
|
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||||
|
dateElementDMy
|
||||||
|
);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isBetween returns valid false when param date is unparseable", () => {
|
||||||
|
const result = validators.isBetween.check(
|
||||||
|
"15-06-2024",
|
||||||
|
{ startDate: "bad-start", endDate: "2024-12-31" },
|
||||||
|
dateElementDMy
|
||||||
|
);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("minRanked", () => {
|
describe("minRanked", () => {
|
||||||
const rankingElement: TSurveyElement = {
|
const rankingElement: TSurveyElement = {
|
||||||
id: "rank1",
|
id: "rank1",
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import type { TResponseDataValue } from "@formbricks/types/responses";
|
import type { TResponseDataValue } from "@formbricks/types/responses";
|
||||||
|
import {
|
||||||
|
DEFAULT_DATE_STORAGE_FORMAT,
|
||||||
|
type TSurveyDateStorageFormat,
|
||||||
|
} from "@formbricks/types/surveys/date-formats";
|
||||||
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import type {
|
import type {
|
||||||
TValidationRuleParams,
|
TValidationRuleParams,
|
||||||
@@ -27,9 +31,38 @@ import type {
|
|||||||
TValidationRuleType,
|
TValidationRuleType,
|
||||||
TValidatorCheckResult,
|
TValidatorCheckResult,
|
||||||
} from "@formbricks/types/surveys/validation-rules";
|
} from "@formbricks/types/surveys/validation-rules";
|
||||||
|
import { parseDateByFormat } from "@/lib/date-format";
|
||||||
import { countSelections } from "./validators/selection-utils";
|
import { countSelections } from "./validators/selection-utils";
|
||||||
import { validateEmail, validatePhone, validateUrl } from "./validators/validation-utils";
|
import { validateEmail, validatePhone, validateUrl } from "./validators/validation-utils";
|
||||||
|
|
||||||
|
function getDateElementFormat(element: TSurveyElement): TSurveyDateStorageFormat {
|
||||||
|
if (element.type === "date" && "format" in element) {
|
||||||
|
return element.format ?? DEFAULT_DATE_STORAGE_FORMAT;
|
||||||
|
}
|
||||||
|
return DEFAULT_DATE_STORAGE_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseForDateComparison(
|
||||||
|
value: string,
|
||||||
|
format: TSurveyDateStorageFormat,
|
||||||
|
element: TSurveyElement,
|
||||||
|
ruleName: string,
|
||||||
|
paramDates: [string] | [string, string]
|
||||||
|
): { valueDate: Date; paramDates: Date[] } | null {
|
||||||
|
const valueDate = parseDateByFormat(value, format);
|
||||||
|
const parsedParams = paramDates.map((d) => parseDateByFormat(d, DEFAULT_DATE_STORAGE_FORMAT));
|
||||||
|
if (valueDate === null) {
|
||||||
|
console.warn(`[date validation] ${ruleName}: could not parse response date`, {
|
||||||
|
elementId: element.id,
|
||||||
|
format,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (parsedParams.includes(null)) return null;
|
||||||
|
return { valueDate, paramDates: parsedParams as Date[] };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic validator interface
|
* Generic validator interface
|
||||||
* Uses type assertions internally to handle the discriminated union params
|
* Uses type assertions internally to handle the discriminated union params
|
||||||
@@ -349,14 +382,18 @@ export const validators: Record<TValidationRuleType, TValidator> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
isLaterThan: {
|
isLaterThan: {
|
||||||
check: (value: TResponseDataValue, params: TValidationRuleParams): TValidatorCheckResult => {
|
check: (
|
||||||
|
value: TResponseDataValue,
|
||||||
|
params: TValidationRuleParams,
|
||||||
|
element: TSurveyElement
|
||||||
|
): TValidatorCheckResult => {
|
||||||
const typedParams = params as TValidationRuleParamsIsLaterThan;
|
const typedParams = params as TValidationRuleParamsIsLaterThan;
|
||||||
// Skip validation if value is empty
|
if (!value || typeof value !== "string" || value === "") return { valid: true };
|
||||||
if (!value || typeof value !== "string" || value === "") {
|
const parsed = parseForDateComparison(value, getDateElementFormat(element), element, "isLaterThan", [
|
||||||
return { valid: true };
|
typedParams.date,
|
||||||
}
|
]);
|
||||||
// Compare dates as strings (YYYY-MM-DD format)
|
if (!parsed) return { valid: false };
|
||||||
return { valid: value > typedParams.date };
|
return { valid: parsed.valueDate.getTime() > parsed.paramDates[0].getTime() };
|
||||||
},
|
},
|
||||||
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
||||||
const typedParams = params as TValidationRuleParamsIsLaterThan;
|
const typedParams = params as TValidationRuleParamsIsLaterThan;
|
||||||
@@ -364,14 +401,18 @@ export const validators: Record<TValidationRuleType, TValidator> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
isEarlierThan: {
|
isEarlierThan: {
|
||||||
check: (value: TResponseDataValue, params: TValidationRuleParams): TValidatorCheckResult => {
|
check: (
|
||||||
|
value: TResponseDataValue,
|
||||||
|
params: TValidationRuleParams,
|
||||||
|
element: TSurveyElement
|
||||||
|
): TValidatorCheckResult => {
|
||||||
const typedParams = params as TValidationRuleParamsIsEarlierThan;
|
const typedParams = params as TValidationRuleParamsIsEarlierThan;
|
||||||
// Skip validation if value is empty
|
if (!value || typeof value !== "string" || value === "") return { valid: true };
|
||||||
if (!value || typeof value !== "string" || value === "") {
|
const parsed = parseForDateComparison(value, getDateElementFormat(element), element, "isEarlierThan", [
|
||||||
return { valid: true };
|
typedParams.date,
|
||||||
}
|
]);
|
||||||
// Compare dates as strings (YYYY-MM-DD format)
|
if (!parsed) return { valid: false };
|
||||||
return { valid: value < typedParams.date };
|
return { valid: parsed.valueDate.getTime() < parsed.paramDates[0].getTime() };
|
||||||
},
|
},
|
||||||
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
||||||
const typedParams = params as TValidationRuleParamsIsEarlierThan;
|
const typedParams = params as TValidationRuleParamsIsEarlierThan;
|
||||||
@@ -379,14 +420,22 @@ export const validators: Record<TValidationRuleType, TValidator> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
isBetween: {
|
isBetween: {
|
||||||
check: (value: TResponseDataValue, params: TValidationRuleParams): TValidatorCheckResult => {
|
check: (
|
||||||
|
value: TResponseDataValue,
|
||||||
|
params: TValidationRuleParams,
|
||||||
|
element: TSurveyElement
|
||||||
|
): TValidatorCheckResult => {
|
||||||
const typedParams = params as TValidationRuleParamsIsBetween;
|
const typedParams = params as TValidationRuleParamsIsBetween;
|
||||||
// Skip validation if value is empty
|
if (!value || typeof value !== "string" || value === "") return { valid: true };
|
||||||
if (!value || typeof value !== "string" || value === "") {
|
const parsed = parseForDateComparison(value, getDateElementFormat(element), element, "isBetween", [
|
||||||
return { valid: true };
|
typedParams.startDate,
|
||||||
}
|
typedParams.endDate,
|
||||||
// Compare dates as strings (YYYY-MM-DD format)
|
]);
|
||||||
return { valid: value > typedParams.startDate && value < typedParams.endDate };
|
if (!parsed) return { valid: false };
|
||||||
|
const t = parsed.valueDate.getTime();
|
||||||
|
return {
|
||||||
|
valid: t > parsed.paramDates[0].getTime() && t < parsed.paramDates[1].getTime(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
||||||
const typedParams = params as TValidationRuleParamsIsBetween;
|
const typedParams = params as TValidationRuleParamsIsBetween;
|
||||||
@@ -394,14 +443,22 @@ export const validators: Record<TValidationRuleType, TValidator> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
isNotBetween: {
|
isNotBetween: {
|
||||||
check: (value: TResponseDataValue, params: TValidationRuleParams): TValidatorCheckResult => {
|
check: (
|
||||||
|
value: TResponseDataValue,
|
||||||
|
params: TValidationRuleParams,
|
||||||
|
element: TSurveyElement
|
||||||
|
): TValidatorCheckResult => {
|
||||||
const typedParams = params as TValidationRuleParamsIsNotBetween;
|
const typedParams = params as TValidationRuleParamsIsNotBetween;
|
||||||
// Skip validation if value is empty
|
if (!value || typeof value !== "string" || value === "") return { valid: true };
|
||||||
if (!value || typeof value !== "string" || value === "") {
|
const parsed = parseForDateComparison(value, getDateElementFormat(element), element, "isNotBetween", [
|
||||||
return { valid: true };
|
typedParams.startDate,
|
||||||
}
|
typedParams.endDate,
|
||||||
// Compare dates as strings (YYYY-MM-DD format)
|
]);
|
||||||
return { valid: value < typedParams.startDate || value > typedParams.endDate };
|
if (!parsed) return { valid: false };
|
||||||
|
const t = parsed.valueDate.getTime();
|
||||||
|
return {
|
||||||
|
valid: t < parsed.paramDates[0].getTime() || t > parsed.paramDates[1].getTime(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
getDefaultMessage: (params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
||||||
const typedParams = params as TValidationRuleParamsIsNotBetween;
|
const typedParams = params as TValidationRuleParamsIsNotBetween;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const config = ({ mode }) => {
|
|||||||
entry: {
|
entry: {
|
||||||
index: resolve(__dirname, "src/index.ts"),
|
index: resolve(__dirname, "src/index.ts"),
|
||||||
validation: resolve(__dirname, "src/validation.ts"),
|
validation: resolve(__dirname, "src/validation.ts"),
|
||||||
|
"date-format": resolve(__dirname, "src/date-format.ts"),
|
||||||
},
|
},
|
||||||
formats: ["es"],
|
formats: ["es"],
|
||||||
},
|
},
|
||||||
|
|||||||
61
packages/types/surveys/date-formats.ts
Normal file
61
packages/types/surveys/date-formats.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Single source of truth for survey date storage formats.
|
||||||
|
* Values are stored in response data as hyphen-separated strings: year, month, day
|
||||||
|
* in the order defined by each format (e.g. y-M-d = YYYY-MM-DD).
|
||||||
|
*
|
||||||
|
* To add a new format: add the id to the tuple and one descriptor (parseOrder + label) to DATE_STORAGE_FORMATS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Supported date storage format ids. Extend this tuple to add new formats. */
|
||||||
|
export const DATE_STORAGE_FORMAT_IDS = ["M-d-y", "d-M-y", "y-M-d"] as const;
|
||||||
|
|
||||||
|
export type TSurveyDateStorageFormat = (typeof DATE_STORAGE_FORMAT_IDS)[number];
|
||||||
|
|
||||||
|
/** Default format (ISO-style). Used when element has no format set. */
|
||||||
|
export const DEFAULT_DATE_STORAGE_FORMAT: TSurveyDateStorageFormat = "y-M-d";
|
||||||
|
|
||||||
|
/** Indices into the split("-") array for [year, month, day]. parts[yearIdx]=year, etc. */
|
||||||
|
export interface DateParseOrder {
|
||||||
|
yearIdx: number;
|
||||||
|
monthIdx: number;
|
||||||
|
dayIdx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Descriptor for one date storage format. outputOrder is derived from parseOrder. */
|
||||||
|
export interface DateFormatDescriptor {
|
||||||
|
parseOrder: DateParseOrder;
|
||||||
|
/** Default display label (e.g. "YYYY-MM-DD"). Apps can override with i18n. */
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Output order derived from parse order: which of [year, month, day] goes in each string position. */
|
||||||
|
export function getOutputOrder(parseOrder: DateParseOrder): [number, number, number] {
|
||||||
|
const out: [number, number, number] = [0, 0, 0];
|
||||||
|
out[parseOrder.yearIdx] = 0;
|
||||||
|
out[parseOrder.monthIdx] = 1;
|
||||||
|
out[parseOrder.dayIdx] = 2;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One struct per format; single place to add or change a format. */
|
||||||
|
export const DATE_STORAGE_FORMATS: Record<TSurveyDateStorageFormat, DateFormatDescriptor> = {
|
||||||
|
"y-M-d": {
|
||||||
|
parseOrder: { yearIdx: 0, monthIdx: 1, dayIdx: 2 },
|
||||||
|
label: "YYYY-MM-DD",
|
||||||
|
},
|
||||||
|
"d-M-y": {
|
||||||
|
parseOrder: { yearIdx: 2, monthIdx: 1, dayIdx: 0 },
|
||||||
|
label: "DD-MM-YYYY",
|
||||||
|
},
|
||||||
|
"M-d-y": {
|
||||||
|
parseOrder: { yearIdx: 2, monthIdx: 0, dayIdx: 1 },
|
||||||
|
label: "MM-DD-YYYY",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All format ids as an array (for iteration, e.g. dropdowns or try-parse).
|
||||||
|
* When used for try-parse without format metadata, the first matching format wins;
|
||||||
|
* order is M-d-y, d-M-y, y-M-d (see parseDateWithFormats in \@formbricks/surveys).
|
||||||
|
*/
|
||||||
|
export const DATE_STORAGE_FORMATS_LIST: TSurveyDateStorageFormat[] = [...DATE_STORAGE_FORMAT_IDS];
|
||||||
@@ -3,6 +3,7 @@ import { ZStorageUrl, ZUrl } from "../common";
|
|||||||
import { ZI18nString } from "../i18n";
|
import { ZI18nString } from "../i18n";
|
||||||
import { ZAllowedFileExtension } from "../storage";
|
import { ZAllowedFileExtension } from "../storage";
|
||||||
import { TSurveyElementTypeEnum } from "./constants";
|
import { TSurveyElementTypeEnum } from "./constants";
|
||||||
|
import { DATE_STORAGE_FORMAT_IDS } from "./date-formats";
|
||||||
import { FORBIDDEN_IDS } from "./validation";
|
import { FORBIDDEN_IDS } from "./validation";
|
||||||
import { ZValidationRules } from "./validation-rules";
|
import { ZValidationRules } from "./validation-rules";
|
||||||
|
|
||||||
@@ -257,7 +258,7 @@ export type TSurveyPictureSelectionElement = z.infer<typeof ZSurveyPictureSelect
|
|||||||
export const ZSurveyDateElement = ZSurveyElementBase.extend({
|
export const ZSurveyDateElement = ZSurveyElementBase.extend({
|
||||||
type: z.literal(TSurveyElementTypeEnum.Date),
|
type: z.literal(TSurveyElementTypeEnum.Date),
|
||||||
html: ZI18nString.optional(),
|
html: ZI18nString.optional(),
|
||||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
format: z.enum(DATE_STORAGE_FORMAT_IDS),
|
||||||
validation: ZValidation.optional(),
|
validation: ZValidation.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ZAllowedFileExtension } from "../storage";
|
|||||||
import { ZBaseStyling } from "../styling";
|
import { ZBaseStyling } from "../styling";
|
||||||
import { type TSurveyBlock, type TSurveyBlockLogicAction, ZSurveyBlocks } from "./blocks";
|
import { type TSurveyBlock, type TSurveyBlockLogicAction, ZSurveyBlocks } from "./blocks";
|
||||||
import { findBlocksWithCyclicLogic } from "./blocks-validation";
|
import { findBlocksWithCyclicLogic } from "./blocks-validation";
|
||||||
|
import { DATE_STORAGE_FORMAT_IDS } from "./date-formats";
|
||||||
import {
|
import {
|
||||||
type TSurveyElement,
|
type TSurveyElement,
|
||||||
TSurveyElementTypeEnum,
|
TSurveyElementTypeEnum,
|
||||||
@@ -560,7 +561,7 @@ export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
|
|||||||
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
|
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
|
||||||
type: z.literal(TSurveyQuestionTypeEnum.Date),
|
type: z.literal(TSurveyQuestionTypeEnum.Date),
|
||||||
html: ZI18nString.optional(),
|
html: ZI18nString.optional(),
|
||||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
format: z.enum(DATE_STORAGE_FORMAT_IDS),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -806,6 +806,9 @@ importers:
|
|||||||
|
|
||||||
packages/survey-ui:
|
packages/survey-ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@formbricks/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../types
|
||||||
'@formkit/auto-animate':
|
'@formkit/auto-animate':
|
||||||
specifier: 0.9.0
|
specifier: 0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
|||||||
@@ -114,7 +114,8 @@
|
|||||||
"@formbricks/logger#build",
|
"@formbricks/logger#build",
|
||||||
"@formbricks/database#build",
|
"@formbricks/database#build",
|
||||||
"@formbricks/storage#build",
|
"@formbricks/storage#build",
|
||||||
"@formbricks/cache#build"
|
"@formbricks/cache#build",
|
||||||
|
"@formbricks/surveys#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@formbricks/web#test:coverage": {
|
"@formbricks/web#test:coverage": {
|
||||||
@@ -122,7 +123,8 @@
|
|||||||
"@formbricks/logger#build",
|
"@formbricks/logger#build",
|
||||||
"@formbricks/database#build",
|
"@formbricks/database#build",
|
||||||
"@formbricks/storage#build",
|
"@formbricks/storage#build",
|
||||||
"@formbricks/cache#build"
|
"@formbricks/cache#build",
|
||||||
|
"@formbricks/surveys#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
Reference in New Issue
Block a user