added validation for contact and address question

This commit is contained in:
Dhruwang
2026-01-12 17:10:21 +05:30
parent 0a134038de
commit ea661657b5
8 changed files with 378 additions and 46 deletions

View File

@@ -11,7 +11,12 @@ import {
TSurveyOpenTextElementInputType,
TValidationLogic,
} from "@formbricks/types/surveys/elements";
import { TValidationRule, TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
import {
TAddressField,
TContactInfoField,
TValidationRule,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
import { TAllowedFileExtension, ALLOWED_FILE_EXTENSIONS } from "@formbricks/types/storage";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
@@ -70,6 +75,35 @@ export const ValidationRulesEditor = ({
const validationRules = validation?.rules ?? [];
const validationLogic = validation?.logic ?? "and";
const { t } = useTranslation();
// Field options for address and contact info elements
const isAddress = elementType === TSurveyElementTypeEnum.Address;
const isContactInfo = elementType === TSurveyElementTypeEnum.ContactInfo;
const needsFieldSelector = isAddress || isContactInfo;
const addressFields: { value: TAddressField; label: string }[] = [
{ value: "addressLine1", label: t("environments.surveys.edit.address_line_1") },
{ value: "addressLine2", label: t("environments.surveys.edit.address_line_2") },
{ value: "city", label: t("environments.surveys.edit.city") },
{ value: "state", label: t("environments.surveys.edit.state") },
{ value: "zip", label: t("environments.surveys.edit.zip") },
{ value: "country", label: t("environments.surveys.edit.country") },
];
const contactInfoFields: { value: TContactInfoField; label: string }[] = [
{ value: "firstName", label: t("environments.surveys.edit.first_name") },
{ value: "lastName", label: t("environments.surveys.edit.last_name") },
{ value: "email", label: t("common.email") },
{ value: "phone", label: t("common.phone") },
{ value: "company", label: t("environments.surveys.edit.company") },
];
let fieldOptions: { value: TAddressField | TContactInfoField; label: string }[] = [];
if (isAddress) {
fieldOptions = addressFields;
} else if (isContactInfo) {
fieldOptions = contactInfoFields;
}
const {
billingInfo,
error: billingInfoError,
@@ -134,10 +168,13 @@ export const ValidationRulesEditor = ({
elementType !== TSurveyElementTypeEnum.Matrix || (element && !element.required);
const handleEnable = () => {
// For address/contact info, get rules for first field
const defaultField = needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined;
const availableRules = getAvailableRuleTypes(
elementType,
[],
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
defaultField
);
if (availableRules.length > 0) {
const defaultRuleType = availableRules[0];
@@ -166,6 +203,8 @@ export const ValidationRulesEditor = ({
id: uuidv7(),
type: defaultRuleType,
params: createRuleParams(defaultRuleType, defaultValue),
// For address/contact info, set field to first available field if not set
field: needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined,
} as TValidationRule;
onUpdateValidation({ rules: [newRule], logic: validationLogic });
}
@@ -176,10 +215,14 @@ export const ValidationRulesEditor = ({
};
const handleAddRule = (insertAfterIndex: number) => {
// For address/contact info, get rules for the field of the rule we're inserting after (or first field)
const insertAfterRule = validationRules[insertAfterIndex];
const fieldForNewRule = insertAfterRule?.field ?? (needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined);
const availableRules = getAvailableRuleTypes(
elementType,
validationRules,
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
fieldForNewRule
);
if (availableRules.length === 0) return;
@@ -209,6 +252,8 @@ export const ValidationRulesEditor = ({
id: uuidv7(),
type: newRuleType,
params: createRuleParams(newRuleType, defaultValue),
// For address/contact info, set field to first available field if not set
field: needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined,
} as TValidationRule;
const newRules = [...validationRules];
newRules.splice(insertAfterIndex + 1, 0, newRule);
@@ -221,6 +266,24 @@ export const ValidationRulesEditor = ({
};
const handleRuleTypeChange = (ruleId: string, newType: TValidationRuleType) => {
const ruleToUpdate = validationRules.find((r) => r.id === ruleId);
if (!ruleToUpdate) return;
// For address/contact info, verify the new rule type is valid for the selected field
if (needsFieldSelector && ruleToUpdate.field) {
const availableRulesForField = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== ruleId),
undefined,
ruleToUpdate.field
);
// If the new rule type is not available for this field, don't change it
if (!availableRulesForField.includes(newType)) {
return;
}
}
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
return {
@@ -232,6 +295,39 @@ export const ValidationRulesEditor = ({
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleFieldChange = (ruleId: string, field: TAddressField | TContactInfoField | undefined) => {
const ruleToUpdate = validationRules.find((r) => r.id === ruleId);
if (!ruleToUpdate) return;
// If changing field, check if current rule type is still valid for the new field
// If not, change to first available rule type for that field
let updatedRule = { ...ruleToUpdate, field } as TValidationRule;
if (field) {
const availableRulesForField = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== ruleId),
undefined,
field
);
// If current rule type is not available for the new field, change it
if (!availableRulesForField.includes(ruleToUpdate.type) && availableRulesForField.length > 0) {
updatedRule = {
...updatedRule,
type: availableRulesForField[0],
params: createRuleParams(availableRulesForField[0]),
} as TValidationRule;
}
}
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
return updatedRule;
});
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleRuleValueChange = (ruleId: string, value: string) => {
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
@@ -348,7 +444,13 @@ export const ValidationRulesEditor = ({
const availableRulesForAdd = getAvailableRuleTypes(
elementType,
validationRules,
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
// For address/contact info, use first field if no rules exist, or use the field from last rule
needsFieldSelector && validationRules.length > 0
? validationRules[validationRules.length - 1]?.field
: needsFieldSelector && fieldOptions.length > 0
? fieldOptions[0].value
: undefined
);
const canAddMore = availableRulesForAdd.length > 0;
@@ -390,10 +492,13 @@ export const ValidationRulesEditor = ({
// Get available types for this rule (current type + unused types, no duplicates)
// For address/contact info, filter by selected field
const ruleField = rule.field;
const otherAvailableTypes = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== rule.id),
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
ruleField
).filter((t) => t !== ruleType);
const availableTypesForSelect = [ruleType, ...otherAvailableTypes];
@@ -415,6 +520,25 @@ export const ValidationRulesEditor = ({
return (
<div key={rule.id} className="flex w-full items-center gap-2">
{/* Field Selector (for Address and Contact Info elements) */}
{needsFieldSelector && (
<Select
value={rule.field ?? ""}
onValueChange={(value) =>
handleFieldChange(rule.id, value ? (value as TAddressField | TContactInfoField) : undefined)
}>
<SelectTrigger className="h-9 min-w-[140px] bg-white">
<SelectValue placeholder={t("environments.surveys.edit.select_field")} />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((field) => (
<SelectItem key={field.value} value={field.value}>
{field.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* Input Type Selector (only for OpenText, first rule) */}
{showInputTypeSelector && inputType !== undefined && onUpdateInputType && (
<Select

View File

@@ -4,10 +4,36 @@ import {
} from "@formbricks/types/surveys/elements";
import {
APPLICABLE_RULES,
TAddressField,
TContactInfoField,
TValidationRule,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
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)
export const RULES_BY_ADDRESS_FIELD: Record<TAddressField, TValidationRuleType[]> = {
addressLine1: stringRules,
addressLine2: stringRules,
city: stringRules,
state: stringRules,
zip: stringRules,
country: stringRules,
};
// Rules applicable per field for Contact Info elements
// Note: "email" and "phone" validation are automatically enforced for their respective fields
// and should not appear as selectable options in the UI
export const RULES_BY_CONTACT_INFO_FIELD: Record<TContactInfoField, TValidationRuleType[]> = {
firstName: stringRules,
lastName: stringRules,
email: stringRules,
phone: ["equals", "doesNotEqual", "contains", "doesNotContain"],
company: stringRules,
};
// Rules applicable per input type for OpenText
export const RULES_BY_INPUT_TYPE: Record<TSurveyOpenTextElementInputType, TValidationRuleType[]> = {
text: [
@@ -65,11 +91,13 @@ export const RULES_BY_INPUT_TYPE: Record<TSurveyOpenTextElementInputType, TValid
/**
* Get available rule types for an element type, excluding already added rules
* For OpenText elements, filters rules based on inputType
* For Address/ContactInfo elements, filters rules based on field
*/
export const getAvailableRuleTypes = (
elementType: TSurveyElementTypeEnum,
existingRules: TValidationRule[],
inputType?: TSurveyOpenTextElementInputType
inputType?: TSurveyOpenTextElementInputType,
field?: TAddressField | TContactInfoField
): TValidationRuleType[] => {
const elementTypeKey = elementType.toString();
@@ -80,6 +108,20 @@ export const getAvailableRuleTypes = (
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
// For Address elements, use field-based filtering
if (elementType === TSurveyElementTypeEnum.Address && field) {
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) {
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));
}
// For other element types, use standard filtering
const applicable = APPLICABLE_RULES[elementTypeKey] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));

View File

@@ -108,41 +108,43 @@ function FormField({
/>
{/* Form Fields */}
<div className="relative space-y-3">
<div className="relative">
<ElementError errorMessage={errorMessage} dir={dir} />
{visibleFields.map((field) => {
const fieldRequired = isFieldRequired(field);
const fieldValue = currentValues[field.id] ?? "";
const fieldInputId = `${elementId}-${field.id}`;
<div className="space-y-3">
{visibleFields.map((field) => {
const fieldRequired = isFieldRequired(field);
const fieldValue = currentValues[field.id] ?? "";
const fieldInputId = `${elementId}-${field.id}`;
// Determine input type
let inputType: "text" | "email" | "tel" | "number" | "url" = field.type ?? "text";
if (field.id === "email" && !field.type) {
inputType = "email";
} else if (field.id === "phone" && !field.type) {
inputType = "tel";
}
// Determine input type
let inputType: "text" | "email" | "tel" | "number" | "url" = field.type ?? "text";
if (field.id === "email" && !field.type) {
inputType = "email";
} else if (field.id === "phone" && !field.type) {
inputType = "tel";
}
return (
<div key={field.id} className="space-y-2">
<Label htmlFor={fieldInputId} variant="default">
{fieldRequired ? `${field.label}*` : field.label}
</Label>
<Input
id={fieldInputId}
type={inputType}
value={fieldValue}
onChange={(e) => {
handleFieldChange(field.id, e.target.value);
}}
required={fieldRequired}
disabled={disabled}
dir={dir}
aria-invalid={Boolean(errorMessage) || undefined}
/>
</div>
);
})}
return (
<div key={field.id} className="space-y-2">
<Label htmlFor={fieldInputId} variant="default">
{fieldRequired ? `${field.label}*` : field.label}
</Label>
<Input
id={fieldInputId}
type={inputType}
value={fieldValue}
onChange={(e) => {
handleFieldChange(field.id, e.target.value);
}}
required={fieldRequired}
disabled={disabled}
dir={dir}
aria-invalid={Boolean(errorMessage) || undefined}
/>
</div>
);
})}
</div>
</div>
</div>
);

View File

@@ -15,6 +15,7 @@ interface AddressElementProps {
currentElementId: string;
autoFocusEnabled: boolean;
dir?: "ltr" | "rtl" | "auto";
errorMessage?: string;
}
export function AddressElement({
@@ -26,6 +27,7 @@ export function AddressElement({
setTtc,
currentElementId,
dir = "auto",
errorMessage,
}: Readonly<AddressElementProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isCurrent = element.id === currentElementId;
@@ -122,6 +124,7 @@ export function AddressElement({
dir={dir}
imageUrl={element.imageUrl}
videoUrl={element.videoUrl}
errorMessage={errorMessage}
/>
</form>
);

View File

@@ -16,6 +16,7 @@ interface ContactInfoElementProps {
currentElementId: string;
autoFocusEnabled: boolean;
dir?: "ltr" | "rtl" | "auto";
errorMessage?: string;
}
export function ContactInfoElement({
@@ -27,6 +28,7 @@ export function ContactInfoElement({
setTtc,
currentElementId,
dir = "auto",
errorMessage,
}: Readonly<ContactInfoElementProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isCurrent = element.id === currentElementId;
@@ -118,6 +120,7 @@ export function ContactInfoElement({
dir={dir}
imageUrl={element.imageUrl}
videoUrl={element.videoUrl}
errorMessage={errorMessage}
/>
</form>
);

View File

@@ -310,6 +310,7 @@ export function ElementConditional({
currentElementId={currentElementId}
autoFocusEnabled={autoFocusEnabled}
dir={dir}
errorMessage={errorMessage}
/>
);
case TSurveyElementTypeEnum.Ranking:
@@ -338,6 +339,7 @@ export function ElementConditional({
currentElementId={currentElementId}
autoFocusEnabled={autoFocusEnabled}
dir={dir}
errorMessage={errorMessage}
/>
);
default:

View File

@@ -1,15 +1,18 @@
import type { TFunction } from "i18next";
import type { TResponseData, TResponseDataValue } from "@formbricks/types/responses";
import type {
TSurveyElement
TSurveyElement,
} from "@formbricks/types/surveys/elements";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import type {
TAddressField,
TContactInfoField,
TValidationError,
TValidationErrorMap,
TValidationResult,
TValidationRule,
} from "@formbricks/types/surveys/validation-rules";
import { getLocalizedValue } from "@/lib/i18n";
import { validators } from "./validators";
/**
@@ -36,6 +39,33 @@ const createRequiredError = (t: TFunction): TValidationError => {
} as TValidationError;
};
/**
* Get field label for address/contact info elements
*/
const getFieldLabel = (
element: TSurveyElement,
field: TAddressField | TContactInfoField | undefined,
languageCode: string
): string | undefined => {
if (!field) return undefined;
if (element.type === TSurveyElementTypeEnum.Address && "addressLine1" in element) {
const fieldConfig = element[field as TAddressField];
if (fieldConfig && "placeholder" in fieldConfig) {
return getLocalizedValue(fieldConfig.placeholder, languageCode);
}
}
if (element.type === TSurveyElementTypeEnum.ContactInfo && "firstName" in element) {
const fieldConfig = element[field as TContactInfoField];
if (fieldConfig && "placeholder" in fieldConfig) {
return getLocalizedValue(fieldConfig.placeholder, languageCode);
}
}
return undefined;
};
/**
* Get default error message from rule or validator
*/
@@ -49,11 +79,21 @@ const getDefaultErrorMessage = (
if (!validator) {
return t("errors.invalid_format");
}
return (
const baseMessage =
rule.customErrorMessage?.[languageCode] ??
rule.customErrorMessage?.default ??
validator.getDefaultMessage(rule.params, element, t)
);
validator.getDefaultMessage(rule.params, element, t);
// For field-specific validation, prepend the field name
if (rule.field) {
const fieldLabel = getFieldLabel(element, rule.field, languageCode);
if (fieldLabel) {
return `${fieldLabel}: ${baseMessage}`;
}
}
return baseMessage;
};
/**
@@ -142,6 +182,31 @@ export const validateElementResponse = (
}
}
// For ContactInfo elements, automatically add email/phone validation for their respective fields
if (element.type === TSurveyElementTypeEnum.ContactInfo) {
const contactInfoElement = element;
// Automatically validate email field if it's shown
if (contactInfoElement.email?.show && !rules.some((r) => r.type === "email" && r.field === "email")) {
rules.push({
id: "__implicit_email_field__",
type: "email",
field: "email",
params: {},
} as TValidationRule);
}
// Automatically validate phone field if it's shown
if (contactInfoElement.phone?.show && !rules.some((r) => r.type === "phone" && r.field === "phone")) {
rules.push({
id: "__implicit_phone_field__",
type: "phone",
field: "phone",
params: {},
} as TValidationRule);
}
}
if (rules.length === 0) {
return { valid: errors.length === 0, errors };
}
@@ -149,6 +214,32 @@ export const validateElementResponse = (
// Get validation logic (default to "and" if not specified)
const validationLogic = validation?.logic ?? "and";
// Helper function to get field value for address/contact info elements
const getFieldValue = (rule: TValidationRule, elementValue: TResponseDataValue): TResponseDataValue => {
// If rule doesn't have a field, validate the whole value
if (!rule.field) {
return elementValue;
}
// For address and contact info, value is an array
if (element.type === TSurveyElementTypeEnum.Address && Array.isArray(elementValue)) {
const addressFieldOrder: TAddressField[] = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
const fieldIndex = addressFieldOrder.indexOf(rule.field as TAddressField);
if (fieldIndex >= 0 && fieldIndex < elementValue.length) {
return elementValue[fieldIndex] ?? "";
}
} else if (element.type === TSurveyElementTypeEnum.ContactInfo && Array.isArray(elementValue)) {
const contactFieldOrder: TContactInfoField[] = ["firstName", "lastName", "email", "phone", "company"];
const fieldIndex = contactFieldOrder.indexOf(rule.field as TContactInfoField);
if (fieldIndex >= 0 && fieldIndex < elementValue.length) {
return elementValue[fieldIndex] ?? "";
}
}
// Fallback: return empty string if field not found
return "";
};
if (validationLogic === "or") {
// OR logic: at least one rule must pass
const ruleResults: { valid: boolean; error?: TValidationError }[] = [];
@@ -162,7 +253,9 @@ export const validateElementResponse = (
continue;
}
const checkResult = validator.check(value, rule.params, element);
// Get the value to validate (field-specific for address/contact info)
const valueToValidate = getFieldValue(rule, value);
const checkResult = validator.check(valueToValidate, rule.params, element);
if (checkResult.valid) {
// At least one rule passed, validation succeeds
@@ -198,7 +291,9 @@ export const validateElementResponse = (
continue;
}
const checkResult = validator.check(value, rule.params, element);
// Get the value to validate (field-specific for address/contact info)
const valueToValidate = getFieldValue(rule, value);
const checkResult = validator.check(valueToValidate, rule.params, element);
if (!checkResult.valid) {
const message = getDefaultErrorMessage(rule, element, languageCode, t);

View File

@@ -1,6 +1,17 @@
import { z } from "zod";
import { ZI18nString } from "../i18n";
// Field types for field-specific validation (address and contact info elements)
export const ZAddressField = z.enum(["addressLine1", "addressLine2", "city", "state", "zip", "country"]);
export type TAddressField = z.infer<typeof ZAddressField>;
export const ZContactInfoField = z.enum(["firstName", "lastName", "email", "phone", "company"]);
export type TContactInfoField = z.infer<typeof ZContactInfoField>;
// Union type for all possible field types
export const ZValidationRuleField = z.union([ZAddressField, ZContactInfoField]);
export type TValidationRuleField = z.infer<typeof ZValidationRuleField>;
// Validation rule type enum - extensible for future rule types
export const ZValidationRuleType = z.enum([
// Text/OpenText rules
@@ -212,162 +223,189 @@ export type TValidationRuleParamsFileExtensionIs = z.infer<typeof ZValidationRul
export type TValidationRuleParamsFileExtensionIsNot = z.infer<typeof ZValidationRuleParamsFileExtensionIsNot>;
// 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(),
}),
]);
@@ -413,8 +451,31 @@ const MATRIX_RULES = ["minRowsAnswered"] as const;
const RANKING_RULES = ["minRanked"] as const;
const FILE_UPLOAD_RULES = ["fileSizeAtLeast", "fileSizeAtMost", "fileExtensionIs", "fileExtensionIsNot"] as const;
const PICTURE_SELECTION_RULES = ["minSelections", "maxSelections"] as const;
const ADDRESS_RULES = [] as const;
const CONTACT_INFO_RULES = [] as const;
// Address and Contact Info can use text-based validation rules on specific fields
const ADDRESS_RULES = [
"minLength",
"maxLength",
"pattern",
"email",
"url",
"phone",
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
] as const;
const CONTACT_INFO_RULES = [
"minLength",
"maxLength",
"pattern",
"email",
"url",
"phone",
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
] as const;
const CAL_RULES = [] as const;
const CTA_RULES = [] as const;