fix: build and test

This commit is contained in:
Dhruwang
2026-01-12 19:05:43 +05:30
parent 539e7a0fc3
commit e1926b46da
15 changed files with 72 additions and 556 deletions

View File

@@ -3,13 +3,11 @@
import { PlusIcon } from "lucide-react";
import { type JSX, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyCalElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurveyCalElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TValidationRulesForCal } from "@formbricks/types/surveys/validation-rules";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
@@ -145,16 +143,6 @@ export const CalElementForm = ({
</AdvancedOptionToggle>
</div>
</div>
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.Cal}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);
};

View File

@@ -4,13 +4,11 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyConsentElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurveyConsentElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TValidationRulesForConsent } from "@formbricks/types/surveys/validation-rules";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
interface ConsentElementFormProps {
@@ -104,16 +102,6 @@ export const ConsentElementForm = ({
placeholder="I agree to the terms and conditions"
value={element.label}
/>
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.Consent}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);
};

View File

@@ -6,7 +6,6 @@ import { type JSX, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyContactInfoElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TValidationRulesForContactInfo } from "@formbricks/types/surveys/validation-rules";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
@@ -162,11 +161,11 @@ export const ContactInfoElementForm = ({
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.ContactInfo}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);

View File

@@ -6,7 +6,6 @@ import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyDateElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TValidationRulesForDate } from "@formbricks/types/surveys/validation-rules";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
@@ -132,11 +131,11 @@ export const DateElementForm = ({
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.Date}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);

View File

@@ -7,7 +7,6 @@ import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyElementTypeEnum, TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TValidationRulesForFileUpload } from "@formbricks/types/surveys/validation-rules";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
@@ -117,11 +116,11 @@ export const FileUploadElementForm = ({
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.FileUpload}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
projectOrganizationId={project?.organizationId}
isFormbricksCloud={isFormbricksCloud}

View File

@@ -4,12 +4,11 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyElementTypeEnum, TSurveyNPSElement } from "@formbricks/types/surveys/elements";
import { TSurveyNPSElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
@@ -141,16 +140,6 @@ export const NPSElementForm = ({
childBorder
customContainerClass="p-0 mt-4"
/>
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.NPS}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);
};

View File

@@ -3,13 +3,12 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TSurveyElementTypeEnum, TSurveyRatingElement } from "@formbricks/types/surveys/elements";
import { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Dropdown } from "@/modules/survey/editor/components/rating-type-dropdown";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
@@ -190,19 +189,6 @@ export const RatingElementForm = ({
customContainerClass="p-0 mt-4"
/>
)}
<ValidationRulesEditor
elementType={TSurveyElementTypeEnum.Rating}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation: {
rules: validation.rules,
logic: validation.logic ?? "and",
},
});
}}
/>
</form>
);
};

View File

@@ -32,7 +32,6 @@ interface ValidationRuleRowProps {
fieldOptions: { value: TAddressField | TContactInfoField; label: string }[];
needsFieldSelector: boolean;
validationRules: TValidationRule[];
effectiveMaxSizeInMB?: number;
ruleLabels: Record<string, string>;
onFieldChange: (ruleId: string, field: TAddressField | TContactInfoField | undefined) => void;
onRuleTypeChange: (ruleId: string, newType: TValidationRuleType) => void;
@@ -54,7 +53,6 @@ export const ValidationRuleRow = ({
fieldOptions,
needsFieldSelector,
validationRules,
effectiveMaxSizeInMB,
ruleLabels,
onFieldChange,
onRuleTypeChange,
@@ -130,7 +128,6 @@ export const ValidationRuleRow = ({
onChange={(value) => onRuleValueChange(rule.id, value)}
onFileExtensionChange={handleFileExtensionChange}
element={element}
effectiveMaxSizeInMB={effectiveMaxSizeInMB}
/>
{/* Unit selector (if applicable) */}

View File

@@ -23,7 +23,6 @@ interface ValidationRuleValueInputProps {
onChange: (value: string) => void;
onFileExtensionChange: (extensions: TAllowedFileExtension[]) => void;
element?: TSurveyElement;
effectiveMaxSizeInMB?: number;
}
export const ValidationRuleValueInput = ({
@@ -34,7 +33,6 @@ export const ValidationRuleValueInput = ({
onChange,
onFileExtensionChange,
element,
effectiveMaxSizeInMB,
}: ValidationRuleValueInputProps) => {
const { t } = useTranslation();

View File

@@ -400,7 +400,6 @@ export const ValidationRulesEditor = ({
fieldOptions={fieldOptions}
needsFieldSelector={needsFieldSelector}
validationRules={validationRules}
effectiveMaxSizeInMB={effectiveMaxSizeInMB}
ruleLabels={ruleLabels}
onFieldChange={handleFieldChange}
onRuleTypeChange={handleRuleTypeChange}

View File

@@ -5,7 +5,6 @@ import { RULE_TYPE_CONFIG } from "./validation-rules-config";
describe("RULE_TYPE_CONFIG", () => {
test("should have config for all validation rule types", () => {
const allRuleTypes: TValidationRuleType[] = [
"required",
"minLength",
"maxLength",
"pattern",
@@ -151,7 +150,6 @@ describe("RULE_TYPE_CONFIG", () => {
});
test("should not have valueType for rules that don't need values", () => {
expect(RULE_TYPE_CONFIG.required.valueType).toBeUndefined();
expect(RULE_TYPE_CONFIG.email.valueType).toBeUndefined();
expect(RULE_TYPE_CONFIG.url.valueType).toBeUndefined();
expect(RULE_TYPE_CONFIG.phone.valueType).toBeUndefined();
@@ -167,7 +165,6 @@ describe("RULE_TYPE_CONFIG", () => {
});
test("should not have unitOptions for other rules", () => {
expect(RULE_TYPE_CONFIG.required.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.pattern.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.email.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.url.unitOptions).toBeUndefined();

View File

@@ -1,7 +1,4 @@
import {
TSurveyElementTypeEnum,
TSurveyOpenTextElementInputType,
} from "@formbricks/types/surveys/elements";
import { TSurveyElementTypeEnum, TSurveyOpenTextElementInputType } from "@formbricks/types/surveys/elements";
import {
APPLICABLE_RULES,
TAddressField,
@@ -10,7 +7,15 @@ import {
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
const stringRules: TValidationRuleType[] = ["minLength", "maxLength", "pattern", "equals", "doesNotEqual", "contains", "doesNotContain"];
const stringRules: TValidationRuleType[] = [
"minLength",
"maxLength",
"pattern",
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
];
// Rules applicable per field for Address elements
// General text fields don't support format-specific validators (email, url, phone)
@@ -40,9 +45,7 @@ export const RULES_BY_INPUT_TYPE: Record<TSurveyOpenTextElementInputType, TValid
"minLength",
"maxLength",
"pattern",
"email",
"url",
"phone",
// "email", "url", "phone" excluded - redundant for text inputType
"equals",
"doesNotEqual",
"contains",
@@ -78,14 +81,7 @@ export const RULES_BY_INPUT_TYPE: Record<TSurveyOpenTextElementInputType, TValid
"contains",
"doesNotContain",
],
number: [
"minValue",
"maxValue",
"isGreaterThan",
"isLessThan",
"equals",
"doesNotEqual",
],
number: ["minValue", "maxValue", "isGreaterThan", "isLessThan", "equals", "doesNotEqual"],
};
/**
@@ -109,14 +105,22 @@ export const getAvailableRuleTypes = (
}
// For Address elements, use field-based filtering
if (elementType === TSurveyElementTypeEnum.Address && field) {
if (elementType === TSurveyElementTypeEnum.Address) {
if (!field) {
// Address elements require a field to be specified for validation rules
return [];
}
const applicable = RULES_BY_ADDRESS_FIELD[field as TAddressField] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
// For Contact Info elements, use field-based filtering
if (elementType === TSurveyElementTypeEnum.ContactInfo && field) {
if (elementType === TSurveyElementTypeEnum.ContactInfo) {
if (!field) {
// Contact Info elements require a field to be specified for validation rules
return [];
}
const applicable = RULES_BY_CONTACT_INFO_FIELD[field as TContactInfoField] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));

View File

@@ -82,7 +82,7 @@ function CTA({
<div className="relative space-y-2">
<ElementError errorMessage={errorMessage} dir={dir} />
{buttonExternal && (
{buttonExternal ? (
<div className="flex w-full justify-start">
<Button
id={inputId}
@@ -95,7 +95,7 @@ function CTA({
<SquareArrowOutUpRightIcon className="size-4" />
</Button>
</div>
)}
) : null}
</div>
</div>
);

View File

@@ -56,13 +56,10 @@ export type TValidationLogic = z.infer<typeof ZValidationLogic>;
// Combined validation object that includes both rules and logic
// Uses general TValidationRule[] type instead of element-specific narrowed types
export const createZValidation = () =>
z
.object({
rules: ZValidationRules,
logic: ZValidationLogic.optional().default("and"),
})
.optional();
export const ZValidation = z.object({
rules: ZValidationRules,
logic: ZValidationLogic.default("and"),
});
// Base element (like ZSurveyQuestionBase but WITHOUT logic, buttonLabel, backButtonLabel)
// Note: validation is not included in base - each element type will add its own narrowed schema
@@ -96,7 +93,7 @@ export const ZSurveyOpenTextElement = ZSurveyElementBase.extend({
max: z.number().optional(),
})
.default({ enabled: false }),
validation: createZValidation(),
validation: ZValidation.optional(),
}).superRefine((data, ctx) => {
if (data.charLimit.enabled && data.charLimit.min === undefined && data.charLimit.max === undefined) {
ctx.addIssue({
@@ -133,7 +130,7 @@ export type TSurveyOpenTextElement = z.infer<typeof ZSurveyOpenTextElement>;
export const ZSurveyConsentElement = ZSurveyElementBase.extend({
type: z.literal(TSurveyElementTypeEnum.Consent),
label: ZI18nString,
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyConsentElement = z.infer<typeof ZSurveyConsentElement>;
@@ -167,7 +164,7 @@ export const ZSurveyMultipleChoiceMultiElement = ZSurveyElementBase.extend({
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
shuffleOption: ZShuffleOption.optional(),
otherOptionPlaceholder: ZI18nString.optional(),
validation: createZValidation(),
validation: ZValidation.optional(),
});
// Union type for Multiple Choice Elements
@@ -184,7 +181,6 @@ export const ZSurveyNPSElement = ZSurveyElementBase.extend({
lowerLabel: ZI18nString.optional(),
upperLabel: ZI18nString.optional(),
isColorCodingEnabled: z.boolean().optional().default(false),
validation: createZValidation(),
});
export type TSurveyNPSElement = z.infer<typeof ZSurveyNPSElement>;
@@ -236,7 +232,6 @@ export const ZSurveyRatingElement = ZSurveyElementBase.extend({
lowerLabel: ZI18nString.optional(),
upperLabel: ZI18nString.optional(),
isColorCodingEnabled: z.boolean().optional().default(false),
validation: createZValidation(),
});
export type TSurveyRatingElement = z.infer<typeof ZSurveyRatingElement>;
@@ -255,7 +250,7 @@ export const ZSurveyPictureSelectionElement = ZSurveyElementBase.extend({
choices: z
.array(ZSurveyPictureChoice)
.min(2, { message: "Picture Selection element must have a minimum of 2 choices" }),
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyPictureSelectionElement = z.infer<typeof ZSurveyPictureSelectionElement>;
@@ -265,7 +260,7 @@ export const ZSurveyDateElement = ZSurveyElementBase.extend({
type: z.literal(TSurveyElementTypeEnum.Date),
html: ZI18nString.optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyDateElement = z.infer<typeof ZSurveyDateElement>;
@@ -276,7 +271,7 @@ export const ZSurveyFileUploadElement = ZSurveyElementBase.extend({
allowMultipleFiles: z.boolean(),
maxSizeInMB: z.number().optional(),
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyFileUploadElement = z.infer<typeof ZSurveyFileUploadElement>;
@@ -286,7 +281,6 @@ export const ZSurveyCalElement = ZSurveyElementBase.extend({
type: z.literal(TSurveyElementTypeEnum.Cal),
calUserName: z.string().min(1, { message: "Cal user name is required" }),
calHost: z.string().optional(),
validation: createZValidation(),
});
export type TSurveyCalElement = z.infer<typeof ZSurveyCalElement>;
@@ -304,7 +298,7 @@ export const ZSurveyMatrixElement = ZSurveyElementBase.extend({
rows: z.array(ZSurveyMatrixElementChoice),
columns: z.array(ZSurveyMatrixElementChoice),
shuffleOption: ZShuffleOption.optional().default("none"),
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyMatrixElement = z.infer<typeof ZSurveyMatrixElement>;
@@ -326,7 +320,7 @@ export const ZSurveyAddressElement = ZSurveyElementBase.extend({
state: ZToggleInputConfig,
zip: ZToggleInputConfig,
country: ZToggleInputConfig,
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyAddressElement = z.infer<typeof ZSurveyAddressElement>;
@@ -340,7 +334,7 @@ export const ZSurveyRankingElement = ZSurveyElementBase.extend({
.max(25, { message: "Ranking Element can have at most 25 options" }),
otherOptionPlaceholder: ZI18nString.optional(),
shuffleOption: ZShuffleOption.optional(),
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyRankingElement = z.infer<typeof ZSurveyRankingElement>;
@@ -353,7 +347,7 @@ export const ZSurveyContactInfoElement = ZSurveyElementBase.extend({
email: ZToggleInputConfig,
phone: ZToggleInputConfig,
company: ZToggleInputConfig,
validation: createZValidation(),
validation: ZValidation.optional(),
});
export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement>;

View File

@@ -224,190 +224,13 @@ export type TValidationRuleParamsFileExtensionIsNot = z.infer<typeof ZValidation
// Validation rule stored on element - discriminated union with type at top level
// Field property is optional and used for address/contact info elements to target specific sub-fields
export const ZValidationRule = z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minLength"),
params: ZValidationRuleParamsMinLength,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxLength"),
params: ZValidationRuleParamsMaxLength,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("pattern"),
params: ZValidationRuleParamsPattern,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("email"),
params: ZValidationRuleParamsEmail,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("url"),
params: ZValidationRuleParamsUrl,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("phone"),
params: ZValidationRuleParamsPhone,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("minValue"),
params: ZValidationRuleParamsMinValue,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxValue"),
params: ZValidationRuleParamsMaxValue,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("minSelections"),
params: ZValidationRuleParamsMinSelections,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxSelections"),
params: ZValidationRuleParamsMaxSelections,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("equals"),
params: ZValidationRuleParamsEquals,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("doesNotEqual"),
params: ZValidationRuleParamsDoesNotEqual,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("contains"),
params: ZValidationRuleParamsContains,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("doesNotContain"),
params: ZValidationRuleParamsDoesNotContain,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isGreaterThan"),
params: ZValidationRuleParamsIsGreaterThan,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isLessThan"),
params: ZValidationRuleParamsIsLessThan,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isLaterThan"),
params: ZValidationRuleParamsIsLaterThan,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isEarlierThan"),
params: ZValidationRuleParamsIsEarlierThan,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isBetween"),
params: ZValidationRuleParamsIsBetween,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isNotBetween"),
params: ZValidationRuleParamsIsNotBetween,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("minRanked"),
params: ZValidationRuleParamsMinRanked,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("minRowsAnswered"),
params: ZValidationRuleParamsMinRowsAnswered,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileSizeAtLeast"),
params: ZValidationRuleParamsFileSizeAtLeast,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileSizeAtMost"),
params: ZValidationRuleParamsFileSizeAtMost,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileExtensionIs"),
params: ZValidationRuleParamsFileExtensionIs,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileExtensionIsNot"),
params: ZValidationRuleParamsFileExtensionIsNot,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
}),
]);
export const ZValidationRule = z.object({
id: z.string(),
type: ZValidationRuleType,
params: ZValidationRuleParams,
customErrorMessage: ZI18nString.optional(),
field: ZValidationRuleField.optional(),
});
export type TValidationRule = z.infer<typeof ZValidationRule>;
@@ -433,23 +256,16 @@ const OPEN_TEXT_RULES = [
"isLessThan",
] as const;
const MULTIPLE_CHOICE_SINGLE_RULES = [] as const;
const MULTIPLE_CHOICE_MULTI_RULES = [
"minSelections",
"maxSelections",
] as const;
const RATING_RULES = [] as const;
const NPS_RULES = [] as const;
const DATE_RULES = [
"isLaterThan",
"isEarlierThan",
"isBetween",
"isNotBetween",
] as const;
const CONSENT_RULES = [] as const;
const MULTIPLE_CHOICE_MULTI_RULES = ["minSelections", "maxSelections"] as const;
const DATE_RULES = ["isLaterThan", "isEarlierThan", "isBetween", "isNotBetween"] as const;
const MATRIX_RULES = ["minRowsAnswered"] as const;
const RANKING_RULES = ["minRanked"] as const;
const FILE_UPLOAD_RULES = ["fileSizeAtLeast", "fileSizeAtMost", "fileExtensionIs", "fileExtensionIsNot"] as const;
const FILE_UPLOAD_RULES = [
"fileSizeAtLeast",
"fileSizeAtMost",
"fileExtensionIs",
"fileExtensionIsNot",
] as const;
const PICTURE_SELECTION_RULES = ["minSelections", "maxSelections"] as const;
// Address and Contact Info can use text-based validation rules on specific fields
const ADDRESS_RULES = [
@@ -476,26 +292,18 @@ const CONTACT_INFO_RULES = [
"contains",
"doesNotContain",
] as const;
const CAL_RULES = [] as const;
const CTA_RULES = [] as const;
// Applicable rules per element type
export const APPLICABLE_RULES: Record<string, TValidationRuleType[]> = {
openText: [...OPEN_TEXT_RULES],
multipleChoiceSingle: [...MULTIPLE_CHOICE_SINGLE_RULES],
multipleChoiceMulti: [...MULTIPLE_CHOICE_MULTI_RULES],
rating: [...RATING_RULES],
nps: [...NPS_RULES],
date: [...DATE_RULES],
consent: [...CONSENT_RULES],
matrix: [...MATRIX_RULES],
ranking: [...RANKING_RULES],
fileUpload: [...FILE_UPLOAD_RULES],
pictureSelection: [...PICTURE_SELECTION_RULES],
address: [...ADDRESS_RULES],
contactInfo: [...CONTACT_INFO_RULES],
cal: [...CAL_RULES],
cta: [...CTA_RULES], // CTA never validates
};
// Type helper to filter validation rules by allowed types
@@ -510,16 +318,10 @@ export type TValidationRulesForElementType<T extends readonly TValidationRuleTyp
// Specific validation rule types for each element type
export type TValidationRulesForOpenText = TValidationRulesForElementType<typeof OPEN_TEXT_RULES>;
export type TValidationRulesForMultipleChoiceSingle = TValidationRulesForElementType<
typeof MULTIPLE_CHOICE_SINGLE_RULES
>;
export type TValidationRulesForMultipleChoiceMulti = TValidationRulesForElementType<
typeof MULTIPLE_CHOICE_MULTI_RULES
>;
export type TValidationRulesForRating = TValidationRulesForElementType<typeof RATING_RULES>;
export type TValidationRulesForNPS = TValidationRulesForElementType<typeof NPS_RULES>;
export type TValidationRulesForDate = TValidationRulesForElementType<typeof DATE_RULES>;
export type TValidationRulesForConsent = TValidationRulesForElementType<typeof CONSENT_RULES>;
export type TValidationRulesForMatrix = TValidationRulesForElementType<typeof MATRIX_RULES>;
export type TValidationRulesForRanking = TValidationRulesForElementType<typeof RANKING_RULES>;
export type TValidationRulesForFileUpload = TValidationRulesForElementType<typeof FILE_UPLOAD_RULES>;
@@ -528,8 +330,6 @@ export type TValidationRulesForPictureSelection = TValidationRulesForElementType
>;
export type TValidationRulesForAddress = TValidationRulesForElementType<typeof ADDRESS_RULES>;
export type TValidationRulesForContactInfo = TValidationRulesForElementType<typeof CONTACT_INFO_RULES>;
export type TValidationRulesForCal = TValidationRulesForElementType<typeof CAL_RULES>;
export type TValidationRulesForCTA = TValidationRulesForElementType<typeof CTA_RULES>;
// Validation error returned by evaluator
export interface TValidationError {
@@ -546,224 +346,3 @@ export interface TValidationResult {
// Error map for block-level validation (keyed by elementId)
export type TValidationErrorMap = Record<string, TValidationError[]>;
// Element-specific validation rules schemas (manually created for type safety)
// These are narrowed versions of ZValidationRule that only include applicable rule types
export const ZValidationRulesForOpenText: z.ZodType<TValidationRulesForOpenText> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minLength"),
params: ZValidationRuleParamsMinLength,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxLength"),
params: ZValidationRuleParamsMaxLength,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("pattern"),
params: ZValidationRuleParamsPattern,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("email"),
params: ZValidationRuleParamsEmail,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("url"),
params: ZValidationRuleParamsUrl,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("phone"),
params: ZValidationRuleParamsPhone,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("minValue"),
params: ZValidationRuleParamsMinValue,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxValue"),
params: ZValidationRuleParamsMaxValue,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("equals"),
params: ZValidationRuleParamsEquals,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("doesNotEqual"),
params: ZValidationRuleParamsDoesNotEqual,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("contains"),
params: ZValidationRuleParamsContains,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("doesNotContain"),
params: ZValidationRuleParamsDoesNotContain,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isGreaterThan"),
params: ZValidationRuleParamsIsGreaterThan,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isLessThan"),
params: ZValidationRuleParamsIsLessThan,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForMultipleChoiceMulti: z.ZodType<TValidationRulesForMultipleChoiceMulti> =
z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minSelections"),
params: ZValidationRuleParamsMinSelections,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxSelections"),
params: ZValidationRuleParamsMaxSelections,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForRating: z.ZodType<TValidationRulesForRating> = z.array(z.never());
export const ZValidationRulesForNPS: z.ZodType<TValidationRulesForNPS> = z.array(z.never());
export const ZValidationRulesForDate: z.ZodType<TValidationRulesForDate> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("isLaterThan"),
params: ZValidationRuleParamsIsLaterThan,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isEarlierThan"),
params: ZValidationRuleParamsIsEarlierThan,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isBetween"),
params: ZValidationRuleParamsIsBetween,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("isNotBetween"),
params: ZValidationRuleParamsIsNotBetween,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForConsent: z.ZodType<TValidationRulesForConsent> = z.array(z.never());
export const ZValidationRulesForMatrix: z.ZodType<TValidationRulesForMatrix> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minRowsAnswered"),
params: ZValidationRuleParamsMinRowsAnswered,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForRanking: z.ZodType<TValidationRulesForRanking> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minRanked"),
params: ZValidationRuleParamsMinRanked,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForFileUpload: z.ZodType<TValidationRulesForFileUpload> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("fileSizeAtLeast"),
params: ZValidationRuleParamsFileSizeAtLeast,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileSizeAtMost"),
params: ZValidationRuleParamsFileSizeAtMost,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileExtensionIs"),
params: ZValidationRuleParamsFileExtensionIs,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("fileExtensionIsNot"),
params: ZValidationRuleParamsFileExtensionIsNot,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForPictureSelection: z.ZodType<TValidationRulesForPictureSelection> = z.array(
z.discriminatedUnion("type", [
z.object({
id: z.string(),
type: z.literal("minSelections"),
params: ZValidationRuleParamsMinSelections,
customErrorMessage: ZI18nString.optional(),
}),
z.object({
id: z.string(),
type: z.literal("maxSelections"),
params: ZValidationRuleParamsMaxSelections,
customErrorMessage: ZI18nString.optional(),
}),
])
);
export const ZValidationRulesForAddress: z.ZodType<TValidationRulesForAddress> = z.array(z.never());
export const ZValidationRulesForContactInfo: z.ZodType<TValidationRulesForContactInfo> = z.array(z.never());
export const ZValidationRulesForCal: z.ZodType<TValidationRulesForCal> = z.array(z.never());
export const ZValidationRulesForCTA: z.ZodType<TValidationRulesForCTA> = z.array(z.never());