mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 10:30:00 -06:00
Compare commits
7 Commits
stable
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed285f6fdf | ||
|
|
93eb18d7d4 | ||
|
|
32ee9bab1d | ||
|
|
70d0f0646c | ||
|
|
fada1aa07f | ||
|
|
eff9435b2b | ||
|
|
f5b28c967d |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { CirclePlayIcon, CopyIcon } from "lucide-react";
|
import { CirclePlayIcon, CopyIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
@@ -34,7 +34,6 @@ export const AnonymousLinksTab = ({
|
|||||||
locale,
|
locale,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
}: AnonymousLinksTabProps) => {
|
}: AnonymousLinksTabProps) => {
|
||||||
const surveyUrlWithCustomSuid = `${surveyUrl}?suId=CUSTOM-ID`;
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -49,6 +48,12 @@ export const AnonymousLinksTab = ({
|
|||||||
pendingAction: () => Promise<void> | void;
|
pendingAction: () => Promise<void> | void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const surveyUrlWithCustomSuid = useMemo(() => {
|
||||||
|
const url = new URL(surveyUrl);
|
||||||
|
url.searchParams.set("suId", "CUSTOM-ID");
|
||||||
|
return url.toString();
|
||||||
|
}, [surveyUrl]);
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
const { singleUse } = survey;
|
const { singleUse } = survey;
|
||||||
const { enabled, isEncrypted } = singleUse ?? {};
|
const { enabled, isEncrypted } = singleUse ?? {};
|
||||||
@@ -177,7 +182,11 @@ export const AnonymousLinksTab = ({
|
|||||||
|
|
||||||
if (!!response?.data?.length) {
|
if (!!response?.data?.length) {
|
||||||
const singleUseIds = response.data;
|
const singleUseIds = response.data;
|
||||||
const surveyLinks = singleUseIds.map((singleUseId) => `${surveyUrl}?suId=${singleUseId}`);
|
const surveyLinks = singleUseIds.map((singleUseId) => {
|
||||||
|
const url = new URL(surveyUrl);
|
||||||
|
url.searchParams.set("suId", singleUseId);
|
||||||
|
return url.toString();
|
||||||
|
});
|
||||||
|
|
||||||
// Create content with just the links
|
// Create content with just the links
|
||||||
const csvContent = surveyLinks.join("\n");
|
const csvContent = surveyLinks.join("\n");
|
||||||
|
|||||||
@@ -1826,7 +1826,7 @@ checksums:
|
|||||||
environments/workspace/general/delete_workspace_settings_description: 411ef100f167fc8fca64e833b6c0d030
|
environments/workspace/general/delete_workspace_settings_description: 411ef100f167fc8fca64e833b6c0d030
|
||||||
environments/workspace/general/error_saving_workspace_information: e7b8022785619ef34de1fb1630b3c476
|
environments/workspace/general/error_saving_workspace_information: e7b8022785619ef34de1fb1630b3c476
|
||||||
environments/workspace/general/only_owners_or_managers_can_delete_workspaces: 58da180cd2610210302d85a9896d80bd
|
environments/workspace/general/only_owners_or_managers_can_delete_workspaces: 58da180cd2610210302d85a9896d80bd
|
||||||
environments/workspace/general/recontact_waiting_time: 8977b5160fbf88c456608982b33e246f
|
environments/workspace/general/recontact_waiting_time: 6873c18d51830e2cadef67cce6a2c95c
|
||||||
environments/workspace/general/recontact_waiting_time_settings_description: ebd64fddbea9387b12c027a18358db7e
|
environments/workspace/general/recontact_waiting_time_settings_description: ebd64fddbea9387b12c027a18358db7e
|
||||||
environments/workspace/general/this_action_cannot_be_undone: 3d8b13374ffd3cefc0f3f7ce077bd9c9
|
environments/workspace/general/this_action_cannot_be_undone: 3d8b13374ffd3cefc0f3f7ce077bd9c9
|
||||||
environments/workspace/general/wait_x_days_before_showing_next_survey: d96228788d32ec23dc0d8c8ba77150a6
|
environments/workspace/general/wait_x_days_before_showing_next_survey: d96228788d32ec23dc0d8c8ba77150a6
|
||||||
|
|||||||
@@ -1935,7 +1935,7 @@
|
|||||||
"delete_workspace_settings_description": "Delete workspace with all surveys, responses, people, actions and attributes. This cannot be undone.",
|
"delete_workspace_settings_description": "Delete workspace with all surveys, responses, people, actions and attributes. This cannot be undone.",
|
||||||
"error_saving_workspace_information": "Error saving workspace information",
|
"error_saving_workspace_information": "Error saving workspace information",
|
||||||
"only_owners_or_managers_can_delete_workspaces": "Only owners or managers can delete workspaces",
|
"only_owners_or_managers_can_delete_workspaces": "Only owners or managers can delete workspaces",
|
||||||
"recontact_waiting_time": "Cooldown Period (scross surveys)",
|
"recontact_waiting_time": "Cooldown Period (across surveys)",
|
||||||
"recontact_waiting_time_settings_description": "Control how frequently users can be surveyed across all Website & App Surveys in this workspace.",
|
"recontact_waiting_time_settings_description": "Control how frequently users can be surveyed across all Website & App Surveys in this workspace.",
|
||||||
"this_action_cannot_be_undone": "This action cannot be undone.",
|
"this_action_cannot_be_undone": "This action cannot be undone.",
|
||||||
"wait_x_days_before_showing_next_survey": "Wait X days before showing next survey:",
|
"wait_x_days_before_showing_next_survey": "Wait X days before showing next survey:",
|
||||||
|
|||||||
@@ -91,6 +91,13 @@ export const EditContactAttributesModal = ({
|
|||||||
return allKeyOptions.filter((option) => !selectedKeys.has(String(option.value)));
|
return allKeyOptions.filter((option) => !selectedKeys.has(String(option.value)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset form when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
form.reset(defaultValues);
|
||||||
|
}
|
||||||
|
}, [open, defaultValues, form]);
|
||||||
|
|
||||||
// Scroll to first error on validation failure
|
// Scroll to first error on validation failure
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
|||||||
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||||
import { getContactAttributes, hasEmailAttribute } from "@/modules/ee/contacts/lib/contact-attributes";
|
import {
|
||||||
|
getContactAttributes,
|
||||||
|
hasEmailAttribute,
|
||||||
|
hasUserIdAttribute,
|
||||||
|
} from "@/modules/ee/contacts/lib/contact-attributes";
|
||||||
|
|
||||||
// Default/system attributes that should not be deleted even if missing from payload
|
// Default/system attributes that should not be deleted even if missing from payload
|
||||||
const DEFAULT_ATTRIBUTES = new Set(["email", "userId", "firstName", "lastName"]);
|
const DEFAULT_ATTRIBUTES = new Set(["email", "userId", "firstName", "lastName"]);
|
||||||
@@ -47,12 +51,28 @@ const deleteAttributes = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates or creates contact attributes.
|
||||||
|
*
|
||||||
|
* @param contactId - The ID of the contact to update
|
||||||
|
* @param userId - The user ID of the contact
|
||||||
|
* @param environmentId - The environment ID
|
||||||
|
* @param contactAttributesParam - The attributes to update/create
|
||||||
|
* @param deleteRemovedAttributes - When true, deletes attributes that exist in DB but are not in the payload.
|
||||||
|
* Use this for UI forms where all attributes are submitted. Default is false (merge behavior) for API calls.
|
||||||
|
*/
|
||||||
export const updateAttributes = async (
|
export const updateAttributes = async (
|
||||||
contactId: string,
|
contactId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
contactAttributesParam: TContactAttributes
|
contactAttributesParam: TContactAttributes,
|
||||||
): Promise<{ success: boolean; messages?: string[]; ignoreEmailAttribute?: boolean }> => {
|
deleteRemovedAttributes: boolean = false
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
messages?: string[];
|
||||||
|
ignoreEmailAttribute?: boolean;
|
||||||
|
ignoreUserIdAttribute?: boolean;
|
||||||
|
}> => {
|
||||||
validateInputs(
|
validateInputs(
|
||||||
[contactId, ZId],
|
[contactId, ZId],
|
||||||
[userId, ZString],
|
[userId, ZString],
|
||||||
@@ -61,23 +81,89 @@ export const updateAttributes = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ignoreEmailAttribute = false;
|
let ignoreEmailAttribute = false;
|
||||||
|
let ignoreUserIdAttribute = false;
|
||||||
|
const messages: string[] = [];
|
||||||
|
|
||||||
// Fetch current attributes, contact attribute keys, and email check in parallel
|
// Fetch current attributes, contact attribute keys, and email/userId checks in parallel
|
||||||
const [currentAttributes, contactAttributeKeys, existingEmailAttribute] = await Promise.all([
|
const [currentAttributes, contactAttributeKeys, existingEmailAttribute, existingUserIdAttribute] =
|
||||||
getContactAttributes(contactId),
|
await Promise.all([
|
||||||
getContactAttributeKeys(environmentId),
|
getContactAttributes(contactId),
|
||||||
contactAttributesParam.email
|
getContactAttributeKeys(environmentId),
|
||||||
? hasEmailAttribute(contactAttributesParam.email, environmentId, contactId)
|
contactAttributesParam.email
|
||||||
: Promise.resolve(null),
|
? hasEmailAttribute(contactAttributesParam.email, environmentId, contactId)
|
||||||
]);
|
: Promise.resolve(null),
|
||||||
|
contactAttributesParam.userId
|
||||||
|
? hasUserIdAttribute(contactAttributesParam.userId, environmentId, contactId)
|
||||||
|
: Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
|
||||||
// Process email existence early
|
// Process email and userId existence early
|
||||||
const { email, ...remainingAttributes } = contactAttributesParam;
|
|
||||||
const contactAttributes = existingEmailAttribute ? remainingAttributes : contactAttributesParam;
|
|
||||||
const emailExists = !!existingEmailAttribute;
|
const emailExists = !!existingEmailAttribute;
|
||||||
|
const userIdExists = !!existingUserIdAttribute;
|
||||||
|
|
||||||
// Delete attributes that were removed (using the deleteAttributes service)
|
// Remove email and/or userId from attributes if they already exist on another contact
|
||||||
await deleteAttributes(contactId, currentAttributes, contactAttributesParam, contactAttributeKeys);
|
let contactAttributes = { ...contactAttributesParam };
|
||||||
|
|
||||||
|
// Determine what the final email and userId values will be after this update
|
||||||
|
// Only consider a value as "submitted" if it was explicitly included in the attributes
|
||||||
|
const emailWasSubmitted = "email" in contactAttributesParam;
|
||||||
|
const userIdWasSubmitted = "userId" in contactAttributesParam;
|
||||||
|
|
||||||
|
const submittedEmail = emailWasSubmitted ? contactAttributes.email?.trim() || "" : null;
|
||||||
|
const submittedUserId = userIdWasSubmitted ? contactAttributes.userId?.trim() || "" : null;
|
||||||
|
|
||||||
|
const currentEmail = currentAttributes.email || "";
|
||||||
|
const currentUserId = currentAttributes.userId || "";
|
||||||
|
|
||||||
|
// Calculate final values:
|
||||||
|
// - If not submitted, keep current value
|
||||||
|
// - If submitted but duplicate exists, keep current value
|
||||||
|
// - If submitted and no duplicate, use submitted value
|
||||||
|
const getFinalEmail = (): string => {
|
||||||
|
if (submittedEmail === null) return currentEmail;
|
||||||
|
if (emailExists) return currentEmail;
|
||||||
|
return submittedEmail;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFinalUserId = (): string => {
|
||||||
|
if (submittedUserId === null) return currentUserId;
|
||||||
|
if (userIdExists) return currentUserId;
|
||||||
|
return submittedUserId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalEmail = getFinalEmail();
|
||||||
|
const finalUserId = getFinalUserId();
|
||||||
|
|
||||||
|
// Ensure at least one of email or userId will have a value after update
|
||||||
|
if (!finalEmail && !finalUserId) {
|
||||||
|
// If both would be empty, preserve the current values
|
||||||
|
if (currentEmail) {
|
||||||
|
contactAttributes.email = currentEmail;
|
||||||
|
}
|
||||||
|
if (currentUserId) {
|
||||||
|
contactAttributes.userId = currentUserId;
|
||||||
|
}
|
||||||
|
messages.push("Either email or userId is required. The existing values were preserved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailExists) {
|
||||||
|
const { email: _email, ...rest } = contactAttributes;
|
||||||
|
contactAttributes = rest;
|
||||||
|
ignoreEmailAttribute = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIdExists) {
|
||||||
|
const { userId: _userId, ...rest } = contactAttributes;
|
||||||
|
contactAttributes = rest;
|
||||||
|
ignoreUserIdAttribute = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete attributes that were removed (only when explicitly requested)
|
||||||
|
// This is used by UI forms where all attributes are submitted
|
||||||
|
// For API calls, we want merge behavior by default (only update passed attributes)
|
||||||
|
if (deleteRemovedAttributes) {
|
||||||
|
await deleteAttributes(contactId, currentAttributes, contactAttributesParam, contactAttributeKeys);
|
||||||
|
}
|
||||||
|
|
||||||
// Create lookup map for attribute keys
|
// Create lookup map for attribute keys
|
||||||
const contactAttributeKeyMap = new Map(contactAttributeKeys.map((ack) => [ack.key, ack]));
|
const contactAttributeKeyMap = new Map(contactAttributeKeys.map((ack) => [ack.key, ack]));
|
||||||
@@ -99,12 +185,12 @@ export const updateAttributes = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let messages: string[] = emailExists
|
|
||||||
? ["The email already exists for this environment and was not updated."]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (emailExists) {
|
if (emailExists) {
|
||||||
ignoreEmailAttribute = true;
|
messages.push("The email already exists for this environment and was not updated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIdExists) {
|
||||||
|
messages.push("The userId already exists for this environment and was not updated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all existing attributes
|
// Update all existing attributes
|
||||||
@@ -159,7 +245,8 @@ export const updateAttributes = async (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
messages,
|
messages: messages.length > 0 ? messages : undefined,
|
||||||
ignoreEmailAttribute,
|
ignoreEmailAttribute,
|
||||||
|
ignoreUserIdAttribute,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId, ZString } from "@formbricks/types/common";
|
||||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
import { ZUserEmail } from "@formbricks/types/user";
|
import { ZUserEmail } from "@formbricks/types/user";
|
||||||
@@ -68,3 +68,31 @@ export const hasEmailAttribute = reactCache(
|
|||||||
return !!contactAttribute;
|
return !!contactAttribute;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const hasUserIdAttribute = reactCache(
|
||||||
|
async (userId: string, environmentId: string, contactId: string): Promise<boolean> => {
|
||||||
|
validateInputs([userId, ZString], [environmentId, ZId], [contactId, ZId]);
|
||||||
|
|
||||||
|
const contactAttribute = await prisma.contactAttribute.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
attributeKey: {
|
||||||
|
key: "userId",
|
||||||
|
environmentId,
|
||||||
|
},
|
||||||
|
value: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
contactId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!contactAttribute;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export interface UpdateContactAttributesResult {
|
|||||||
updatedAttributeKeys?: TContactAttributeKey[];
|
updatedAttributeKeys?: TContactAttributeKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates contact attributes for a single contact.
|
|
||||||
* Handles loading contact data, extracting userId, calling updateAttributes,
|
|
||||||
* and detecting if new attribute keys were created.
|
|
||||||
*/
|
|
||||||
export const updateContactAttributes = async (
|
export const updateContactAttributes = async (
|
||||||
contactId: string,
|
contactId: string,
|
||||||
attributes: TContactAttributes
|
attributes: TContactAttributes
|
||||||
@@ -35,16 +30,13 @@ export const updateContactAttributes = async (
|
|||||||
const userId = attributes.userId ?? "";
|
const userId = attributes.userId ?? "";
|
||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
|
|
||||||
if (!attributes.userId) {
|
|
||||||
messages.push("Warning: userId attribute is missing. Some operations may not work correctly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current attribute keys before update to detect new ones
|
// Get current attribute keys before update to detect new ones
|
||||||
const currentAttributeKeys = await getContactAttributeKeys(environmentId);
|
const currentAttributeKeys = await getContactAttributeKeys(environmentId);
|
||||||
const currentKeysSet = new Set(currentAttributeKeys.map((key) => key.key));
|
const currentKeysSet = new Set(currentAttributeKeys.map((key) => key.key));
|
||||||
|
|
||||||
// Call the existing updateAttributes function
|
// Call updateAttributes with deleteRemovedAttributes: true
|
||||||
const updateResult = await updateAttributes(contactId, userId, environmentId, attributes);
|
// UI forms submit all attributes, so any missing attribute should be deleted
|
||||||
|
const updateResult = await updateAttributes(contactId, userId, environmentId, attributes, true);
|
||||||
|
|
||||||
// Merge any messages from updateAttributes
|
// Merge any messages from updateAttributes
|
||||||
if (updateResult.messages) {
|
if (updateResult.messages) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
|||||||
import { TContactWithAttributes, TTransformPersonInput } from "@/modules/ee/contacts/types/contact";
|
import { TContactWithAttributes, TTransformPersonInput } from "@/modules/ee/contacts/types/contact";
|
||||||
|
|
||||||
export const getContactIdentifier = (contactAttributes: TContactAttributes | null): string => {
|
export const getContactIdentifier = (contactAttributes: TContactAttributes | null): string => {
|
||||||
return contactAttributes?.email ?? contactAttributes?.userId ?? "";
|
return contactAttributes?.email || contactAttributes?.userId || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertPrismaContactAttributes = (
|
export const convertPrismaContactAttributes = (
|
||||||
|
|||||||
@@ -335,7 +335,34 @@ export const ZEditContactAttributesForm = z.object({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate email format if key is "email"
|
// Check that at least one of email or userId has a value
|
||||||
|
const emailAttr = attributes.find((attr) => attr.key === "email");
|
||||||
|
const userIdAttr = attributes.find((attr) => attr.key === "userId");
|
||||||
|
const hasEmail = emailAttr?.value && emailAttr.value.trim() !== "";
|
||||||
|
const hasUserId = userIdAttr?.value && userIdAttr.value.trim() !== "";
|
||||||
|
|
||||||
|
if (!hasEmail && !hasUserId) {
|
||||||
|
// Find the indices to show errors on the relevant fields
|
||||||
|
const emailIndex = attributes.findIndex((attr) => attr.key === "email");
|
||||||
|
const userIdIndex = attributes.findIndex((attr) => attr.key === "userId");
|
||||||
|
|
||||||
|
// When both are empty, show "Either email or userId is required" on both fields
|
||||||
|
if (emailIndex !== -1 && userIdIndex !== -1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Either email or userId is required",
|
||||||
|
path: [emailIndex, "value"],
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Either email or userId is required",
|
||||||
|
path: [userIdIndex, "value"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format if key is "email" and has a value
|
||||||
attributes.forEach((attr, index) => {
|
attributes.forEach((attr, index) => {
|
||||||
if (attr.key === "email" && attr.value && attr.value.trim() !== "") {
|
if (attr.key === "email" && attr.value && attr.value.trim() !== "") {
|
||||||
const emailResult = z.string().email().safeParse(attr.value);
|
const emailResult = z.string().email().safeParse(attr.value);
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ function DateElement({
|
|||||||
onSelect={handleDateSelect}
|
onSelect={handleDateSelect}
|
||||||
locale={dateLocale}
|
locale={dateLocale}
|
||||||
required={required}
|
required={required}
|
||||||
className="rounded-input border-input-border bg-input-bg text-input-text shadow-input mx-auto w-full max-w-[25rem] border"
|
className="rounded-input border-input-border bg-input-bg text-input-text shadow-input mx-auto h-[stretch] w-full max-w-[25rem] border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user