mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-17 01:19:32 -06:00
Compare commits
10 Commits
codex/upda
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
474ae4478f | ||
|
|
a01107521e | ||
|
|
2e17e70c00 | ||
|
|
641b597ddc | ||
|
|
b6986e5470 | ||
|
|
82a5eed6e5 | ||
|
|
0f20a4460f | ||
|
|
95ae35a3b5 | ||
|
|
ddd91607b1 | ||
|
|
325eeb10ef |
@@ -9,9 +9,30 @@ import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service"
|
|||||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { Result, ok } from "@formbricks/types/errorHandlers";
|
||||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
params: { environmentId: string; productId: string };
|
||||||
|
response?: Result<TProduct>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProductFormAction = async (state: State, data: FormData): Promise<State> => {
|
||||||
|
console.log({ state });
|
||||||
|
const formData = Object.fromEntries(data);
|
||||||
|
console.log({ formData });
|
||||||
|
|
||||||
|
const updatedProduct = await updateProductAction(state.params.environmentId, state.params.productId, {
|
||||||
|
name: formData.name as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
response: ok(updatedProduct),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const updateProductAction = async (
|
export const updateProductAction = async (
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
productId: string,
|
productId: string,
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { TProduct } from "@formbricks/types/product";
|
|
||||||
import { Button } from "@formbricks/ui/Button";
|
|
||||||
import { Input } from "@formbricks/ui/Input";
|
|
||||||
import { Label } from "@formbricks/ui/Label";
|
|
||||||
|
|
||||||
import { updateProductAction } from "../actions";
|
|
||||||
|
|
||||||
type TEditProductName = {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EditProductNameProps = {
|
|
||||||
product: TProduct;
|
|
||||||
environmentId: string;
|
|
||||||
isProductNameEditDisabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditProductName: React.FC<EditProductNameProps> = ({
|
|
||||||
product,
|
|
||||||
environmentId,
|
|
||||||
isProductNameEditDisabled,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
watch,
|
|
||||||
} = useForm<TEditProductName>({
|
|
||||||
defaultValues: {
|
|
||||||
name: product.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const productNameValue = watch("name", product.name || "");
|
|
||||||
const isNotEmptySpaces = (value: string) => value.trim() !== "";
|
|
||||||
|
|
||||||
const updateProduct: SubmitHandler<TEditProductName> = async (data) => {
|
|
||||||
data.name = data.name.trim();
|
|
||||||
try {
|
|
||||||
if (!isNotEmptySpaces(data.name)) {
|
|
||||||
toast.error("Please enter at least one character");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.name === product.name) {
|
|
||||||
toast.success("This is already your product name");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updatedProduct = await updateProductAction(environmentId, product.id, { name: data.name });
|
|
||||||
if (isProductNameEditDisabled) {
|
|
||||||
toast.error("Only Owners, Admins and Editors can perform this action.");
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!updatedProduct?.id) {
|
|
||||||
toast.success("Product name updated successfully.");
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error(`Error: Unable to save product information`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return !isProductNameEditDisabled ? (
|
|
||||||
<form className="w-full max-w-sm items-center space-y-2" onSubmit={handleSubmit(updateProduct)}>
|
|
||||||
<Label htmlFor="fullname">What's your product called?</Label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id="fullname"
|
|
||||||
defaultValue={product.name}
|
|
||||||
{...register("name", { required: true })}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="darkCTA"
|
|
||||||
size="sm"
|
|
||||||
loading={isSubmitting}
|
|
||||||
disabled={!isNotEmptySpaces(productNameValue) || isSubmitting}>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-red-700">Only Owners, Admins and Editors can perform this action.</p>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useFormState } from "react-dom";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||||
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormProvider } from "@formbricks/ui/Form";
|
||||||
|
import { Input } from "@formbricks/ui/Input";
|
||||||
|
|
||||||
|
import { updateProductAction, updateProductFormAction } from "../actions";
|
||||||
|
import { SubmitButton } from "./SubmitBtn";
|
||||||
|
|
||||||
|
type EditProductNameProps = {
|
||||||
|
product: TProduct;
|
||||||
|
environmentId: string;
|
||||||
|
isProductNameEditDisabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZProductNameInput = ZProduct.pick({ name: true });
|
||||||
|
|
||||||
|
type TEditProductName = z.infer<typeof ZProductNameInput>;
|
||||||
|
|
||||||
|
export const EditProductNameForm: React.FC<EditProductNameProps> = ({
|
||||||
|
product,
|
||||||
|
environmentId,
|
||||||
|
isProductNameEditDisabled,
|
||||||
|
}) => {
|
||||||
|
const form = useForm<TEditProductName>({
|
||||||
|
defaultValues: {
|
||||||
|
name: product.name,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZProductNameInput),
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const [serverState, formAction] = useFormState(updateProductFormAction, {
|
||||||
|
params: { environmentId, productId: product.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { errors, isDirty } = form.formState;
|
||||||
|
|
||||||
|
const nameError = errors.name?.message;
|
||||||
|
// const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
|
// const updateProduct: SubmitHandler<TEditProductName> = async (data) => {
|
||||||
|
// const name = data.name.trim();
|
||||||
|
// try {
|
||||||
|
// if (nameError) {
|
||||||
|
// toast.error(nameError);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const updatedProduct = await updateProductAction(environmentId, product.id, { name });
|
||||||
|
|
||||||
|
// if (isProductNameEditDisabled) {
|
||||||
|
// toast.error("Only Owners, Admins and Editors can perform this action.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!!updatedProduct?.id) {
|
||||||
|
// toast.success("Product name updated successfully.");
|
||||||
|
// form.resetField("name", { defaultValue: updatedProduct.name });
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// toast.error(`Error: Unable to save product information`);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
return !isProductNameEditDisabled ? (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
className="w-full max-w-sm items-center space-y-2"
|
||||||
|
action={formAction}
|
||||||
|
onSubmit={(e) =>
|
||||||
|
form.handleSubmit(() => {
|
||||||
|
e.preventDefault();
|
||||||
|
formRef.current?.submit();
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel htmlFor="name">What's your product called?</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
{...field}
|
||||||
|
placeholder="Product Name"
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
isInvalid={!!nameError}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SubmitButton />
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-red-700">Only Owners, Admins and Editors can perform this action.</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { TProduct } from "@formbricks/types/product";
|
|
||||||
import { Button } from "@formbricks/ui/Button";
|
|
||||||
import { Input } from "@formbricks/ui/Input";
|
|
||||||
import { Label } from "@formbricks/ui/Label";
|
|
||||||
|
|
||||||
import { updateProductAction } from "../actions";
|
|
||||||
|
|
||||||
type EditWaitingTimeFormValues = {
|
|
||||||
recontactDays: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EditWaitingTimeProps = {
|
|
||||||
environmentId: string;
|
|
||||||
product: TProduct;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditWaitingTime: React.FC<EditWaitingTimeProps> = ({ product, environmentId }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<EditWaitingTimeFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
recontactDays: product.recontactDays,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateWaitingTime: SubmitHandler<EditWaitingTimeFormValues> = async (data) => {
|
|
||||||
try {
|
|
||||||
const updatedProduct = await updateProductAction(environmentId, product.id, data);
|
|
||||||
if (!!updatedProduct?.id) {
|
|
||||||
toast.success("Waiting period updated successfully.");
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(`Error: ${err.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="w-full max-w-sm items-center space-y-2" onSubmit={handleSubmit(updateWaitingTime)}>
|
|
||||||
<Label htmlFor="recontactDays">Wait X days before showing next survey:</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="recontactDays"
|
|
||||||
defaultValue={product.recontactDays}
|
|
||||||
{...register("recontactDays", {
|
|
||||||
min: { value: 0, message: "Must be a positive number" },
|
|
||||||
max: { value: 365, message: "Must be less than 365" },
|
|
||||||
valueAsNumber: true,
|
|
||||||
required: {
|
|
||||||
value: true,
|
|
||||||
message: "Required",
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errors?.recontactDays ? (
|
|
||||||
<div className="my-2">
|
|
||||||
<p className="text-xs text-red-500">{errors?.recontactDays?.message}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Button type="submit" variant="darkCTA" size="sm">
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||||
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormProvider } from "@formbricks/ui/Form";
|
||||||
|
import { Input } from "@formbricks/ui/Input";
|
||||||
|
|
||||||
|
import { updateProductAction } from "../actions";
|
||||||
|
|
||||||
|
type EditWaitingTimeProps = {
|
||||||
|
environmentId: string;
|
||||||
|
product: TProduct;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZProductRecontactDaysInput = ZProduct.pick({ recontactDays: true });
|
||||||
|
|
||||||
|
type EditWaitingTimeFormValues = z.infer<typeof ZProductRecontactDaysInput>;
|
||||||
|
|
||||||
|
export const EditWaitingTimeForm: React.FC<EditWaitingTimeProps> = ({ product, environmentId }) => {
|
||||||
|
const form = useForm<EditWaitingTimeFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
recontactDays: product.recontactDays,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZProductRecontactDaysInput),
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isDirty, isSubmitting } = form.formState;
|
||||||
|
|
||||||
|
const updateWaitingTime: SubmitHandler<EditWaitingTimeFormValues> = async (data) => {
|
||||||
|
try {
|
||||||
|
const updatedProduct = await updateProductAction(environmentId, product.id, data);
|
||||||
|
if (!!updatedProduct?.id) {
|
||||||
|
toast.success("Waiting period updated successfully.");
|
||||||
|
form.resetField("recontactDays", { defaultValue: updatedProduct.recontactDays });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
className="flex w-full max-w-sm flex-col space-y-4"
|
||||||
|
onSubmit={form.handleSubmit(updateWaitingTime)}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="recontactDays"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel htmlFor="recontactDays">Wait X days before showing next survey:</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="recontactDays"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange("");
|
||||||
|
}
|
||||||
|
|
||||||
|
field.onChange(parseInt(value, 10));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="darkCTA"
|
||||||
|
size="sm"
|
||||||
|
className="w-fit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting || !isDirty}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useFormStatus } from "react-dom";
|
||||||
|
|
||||||
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
|
||||||
|
export const SubmitButton = () => {
|
||||||
|
const formStatus = useFormStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button type="submit" variant="darkCTA" size="sm" loading={formStatus.pending}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,8 +15,8 @@ import { SettingsId } from "@formbricks/ui/SettingsId";
|
|||||||
|
|
||||||
import { SettingsCard } from "../../settings/components/SettingsCard";
|
import { SettingsCard } from "../../settings/components/SettingsCard";
|
||||||
import { DeleteProduct } from "./components/DeleteProduct";
|
import { DeleteProduct } from "./components/DeleteProduct";
|
||||||
import { EditProductName } from "./components/EditProductName";
|
import { EditProductNameForm } from "./components/EditProductNameForm";
|
||||||
import { EditWaitingTime } from "./components/EditWaitingTime";
|
import { EditWaitingTimeForm } from "./components/EditWaitingTimeForm";
|
||||||
|
|
||||||
const Page = async ({ params }: { params: { environmentId: string } }) => {
|
const Page = async ({ params }: { params: { environmentId: string } }) => {
|
||||||
const [, product, session, team] = await Promise.all([
|
const [, product, session, team] = await Promise.all([
|
||||||
@@ -57,7 +57,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<SettingsCard title="Product Name" description="Change your products name.">
|
<SettingsCard title="Product Name" description="Change your products name.">
|
||||||
<EditProductName
|
<EditProductNameForm
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
product={product}
|
product={product}
|
||||||
isProductNameEditDisabled={isProductNameEditDisabled}
|
isProductNameEditDisabled={isProductNameEditDisabled}
|
||||||
@@ -66,7 +66,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
|||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="Recontact Waiting Time"
|
title="Recontact Waiting Time"
|
||||||
description="Control how frequently users can be surveyed across all surveys.">
|
description="Control how frequently users can be surveyed across all surveys.">
|
||||||
<EditWaitingTime environmentId={params.environmentId} product={product} />
|
<EditWaitingTimeForm environmentId={params.environmentId} product={product} />
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="Delete Product"
|
title="Delete Product"
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TPlacement } from "@formbricks/types/common";
|
|
||||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
|
||||||
import { Button } from "@formbricks/ui/Button";
|
|
||||||
import { Label } from "@formbricks/ui/Label";
|
|
||||||
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
|
||||||
|
|
||||||
import { updateProductAction } from "../actions";
|
|
||||||
|
|
||||||
const placements = [
|
|
||||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
|
||||||
{ name: "Top Right", value: "topRight", disabled: false },
|
|
||||||
{ name: "Top Left", value: "topLeft", disabled: false },
|
|
||||||
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
|
|
||||||
{ name: "Centered Modal", value: "center", disabled: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface EditPlacementProps {
|
|
||||||
product: TProduct;
|
|
||||||
environmentId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditPlacement = ({ product }: EditPlacementProps) => {
|
|
||||||
const [currentPlacement, setCurrentPlacement] = useState<TPlacement>(product.placement);
|
|
||||||
const [overlay, setOverlay] = useState(product.darkOverlay ? "darkOverlay" : "lightOverlay");
|
|
||||||
const [clickOutside, setClickOutside] = useState(product.clickOutsideClose ? "allow" : "disallow");
|
|
||||||
const [updatingPlacement, setUpdatingPlacement] = useState(false);
|
|
||||||
const overlayStyle =
|
|
||||||
currentPlacement === "center" && overlay === "darkOverlay" ? "bg-gray-700/80" : "bg-slate-200";
|
|
||||||
|
|
||||||
const handleUpdatePlacement = async () => {
|
|
||||||
try {
|
|
||||||
setUpdatingPlacement(true);
|
|
||||||
let inputProduct: Partial<TProductUpdateInput> = {
|
|
||||||
placement: currentPlacement,
|
|
||||||
darkOverlay: overlay === "darkOverlay",
|
|
||||||
clickOutsideClose: clickOutside === "allow",
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateProductAction(product.id, inputProduct);
|
|
||||||
|
|
||||||
toast.success("Placement updated successfully.");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(`Error: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
setUpdatingPlacement(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full items-center">
|
|
||||||
<div className="flex">
|
|
||||||
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as TPlacement)} value={currentPlacement}>
|
|
||||||
{placements.map((placement) => (
|
|
||||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
|
|
||||||
<Label htmlFor={placement.value} className="text-slate-900">
|
|
||||||
{placement.name}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
clickOutside === "disallow" ? "cursor-not-allowed" : "",
|
|
||||||
"relative ml-8 h-40 w-full rounded",
|
|
||||||
overlayStyle
|
|
||||||
)}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute h-16 w-16 cursor-default rounded bg-slate-700",
|
|
||||||
getPlacementStyle(currentPlacement)
|
|
||||||
)}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentPlacement === "center" && (
|
|
||||||
<>
|
|
||||||
<div className="mt-6 space-y-2">
|
|
||||||
<Label className="font-semibold">Centered modal overlay color</Label>
|
|
||||||
<RadioGroup onValueChange={(e) => setOverlay(e)} value={overlay} className="flex space-x-4">
|
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<RadioGroupItem id="lightOverlay" value="lightOverlay" />
|
|
||||||
<Label htmlFor="lightOverlay" className="text-slate-900">
|
|
||||||
Light Overlay
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<RadioGroupItem id="darkOverlay" value="darkOverlay" />
|
|
||||||
<Label htmlFor="darkOverlay" className="text-slate-900">
|
|
||||||
Dark Overlay
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 space-y-2">
|
|
||||||
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={(e) => setClickOutside(e)}
|
|
||||||
value={clickOutside}
|
|
||||||
className="flex space-x-4">
|
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<RadioGroupItem id="disallow" value="disallow" />
|
|
||||||
<Label htmlFor="disallow" className="text-slate-900">
|
|
||||||
Don't Allow
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<RadioGroupItem id="allow" value="allow" />
|
|
||||||
<Label htmlFor="allow" className="text-slate-900">
|
|
||||||
Allow
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="darkCTA"
|
|
||||||
className="mt-4"
|
|
||||||
size="sm"
|
|
||||||
loading={updatingPlacement}
|
|
||||||
onClick={handleUpdatePlacement}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
import { TProduct } from "@formbricks/types/product";
|
||||||
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||||
|
import { Label } from "@formbricks/ui/Label";
|
||||||
|
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||||
|
|
||||||
|
import { updateProductAction } from "../actions";
|
||||||
|
|
||||||
|
const placements = [
|
||||||
|
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||||
|
{ name: "Top Right", value: "topRight", disabled: false },
|
||||||
|
{ name: "Top Left", value: "topLeft", disabled: false },
|
||||||
|
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
|
||||||
|
{ name: "Centered Modal", value: "center", disabled: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface EditPlacementProps {
|
||||||
|
product: TProduct;
|
||||||
|
environmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZProductPlacementInput = z.object({
|
||||||
|
placement: z.enum(["bottomRight", "topRight", "topLeft", "bottomLeft", "center"]),
|
||||||
|
darkOverlay: z.boolean(),
|
||||||
|
clickOutsideClose: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type EditPlacementFormValues = z.infer<typeof ZProductPlacementInput>;
|
||||||
|
|
||||||
|
export const EditPlacementForm = ({ product }: EditPlacementProps) => {
|
||||||
|
const form = useForm<EditPlacementFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
placement: product.placement,
|
||||||
|
darkOverlay: product.darkOverlay ?? false,
|
||||||
|
clickOutsideClose: product.clickOutsideClose ?? false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZProductPlacementInput),
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentPlacement = form.watch("placement");
|
||||||
|
const darkOverlay = form.watch("darkOverlay");
|
||||||
|
const clickOutsideClose = form.watch("clickOutsideClose");
|
||||||
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
|
const overlayStyle = currentPlacement === "center" && darkOverlay ? "bg-gray-700/80" : "bg-slate-200";
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
|
||||||
|
try {
|
||||||
|
await updateProductAction(product.id, {
|
||||||
|
placement: data.placement,
|
||||||
|
darkOverlay: data.darkOverlay,
|
||||||
|
clickOutsideClose: data.clickOutsideClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Placement updated successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form className="w-full items-center" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="placement"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
className="h-full">
|
||||||
|
{placements.map((placement) => (
|
||||||
|
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<RadioGroupItem
|
||||||
|
id={placement.value}
|
||||||
|
value={placement.value}
|
||||||
|
disabled={placement.disabled}
|
||||||
|
checked={field.value === placement.value}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={placement.value} className="text-slate-900">
|
||||||
|
{placement.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
clickOutsideClose ? "" : "cursor-not-allowed",
|
||||||
|
"relative ml-8 h-40 w-full rounded",
|
||||||
|
overlayStyle
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute h-16 w-16 cursor-default rounded bg-slate-700",
|
||||||
|
getPlacementStyle(currentPlacement)
|
||||||
|
)}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentPlacement === "center" && (
|
||||||
|
<>
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="darkOverlay"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="font-semibold">Centered modal overlay color</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value === "darkOverlay");
|
||||||
|
}}
|
||||||
|
className="flex space-x-4">
|
||||||
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<RadioGroupItem id="lightOverlay" value="lightOverlay" checked={!field.value} />
|
||||||
|
<Label htmlFor="lightOverlay" className="text-slate-900">
|
||||||
|
Light Overlay
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<RadioGroupItem id="darkOverlay" value="darkOverlay" checked={field.value} />
|
||||||
|
<Label htmlFor="darkOverlay" className="text-slate-900">
|
||||||
|
Dark Overlay
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clickOutsideClose"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="font-semibold">
|
||||||
|
Allow users to exit by clicking outside the study
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value === "allow");
|
||||||
|
}}
|
||||||
|
className="flex space-x-4">
|
||||||
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<RadioGroupItem id="disallow" value="disallow" checked={!field.value} />
|
||||||
|
<Label htmlFor="disallow" className="text-slate-900">
|
||||||
|
Don't Allow
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
|
<RadioGroupItem id="allow" value="allow" checked={field.value} />
|
||||||
|
<Label htmlFor="allow" className="text-slate-900">
|
||||||
|
Allow
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="darkCTA" className="mt-4 w-fit" size="sm" loading={isSubmitting}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
|||||||
|
|
||||||
import { SettingsCard } from "../../settings/components/SettingsCard";
|
import { SettingsCard } from "../../settings/components/SettingsCard";
|
||||||
import { EditFormbricksBranding } from "./components/EditBranding";
|
import { EditFormbricksBranding } from "./components/EditBranding";
|
||||||
import { EditPlacement } from "./components/EditPlacement";
|
import { EditPlacementForm } from "./components/EditPlacementForm";
|
||||||
import { ThemeStyling } from "./components/ThemeStyling";
|
import { ThemeStyling } from "./components/ThemeStyling";
|
||||||
|
|
||||||
const Page = async ({ params }: { params: { environmentId: string } }) => {
|
const Page = async ({ params }: { params: { environmentId: string } }) => {
|
||||||
@@ -77,7 +77,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
|||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="In-app Survey Placement"
|
title="In-app Survey Placement"
|
||||||
description="Change where surveys will be shown in your web app.">
|
description="Change where surveys will be shown in your web app.">
|
||||||
<EditPlacement product={product} environmentId={params.environmentId} />
|
<EditPlacementForm product={product} environmentId={params.environmentId} />
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="Formbricks Branding"
|
title="Formbricks Branding"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@formbricks/tailwind-config": "workspace:*",
|
"@formbricks/tailwind-config": "workspace:*",
|
||||||
"@formbricks/types": "workspace:*",
|
"@formbricks/types": "workspace:*",
|
||||||
"@formbricks/ui": "workspace:*",
|
"@formbricks/ui": "workspace:*",
|
||||||
|
"@hookform/resolvers": "^3.4.2",
|
||||||
"@json2csv/node": "^7.0.6",
|
"@json2csv/node": "^7.0.6",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.46.1",
|
"@opentelemetry/auto-instrumentations-node": "^0.46.1",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.51.1",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.51.1",
|
||||||
|
|||||||
@@ -41,10 +41,14 @@ export const ZProduct = z.object({
|
|||||||
id: z.string().cuid2(),
|
id: z.string().cuid2(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
name: z.string(),
|
name: z.string().trim().min(1, { message: "Product name cannot be empty" }),
|
||||||
teamId: z.string(),
|
teamId: z.string(),
|
||||||
styling: ZProductStyling,
|
styling: ZProductStyling,
|
||||||
recontactDays: z.number().int(),
|
recontactDays: z
|
||||||
|
.number({ message: "Recontact days is required" })
|
||||||
|
.int()
|
||||||
|
.min(0, { message: "Must be a positive number" })
|
||||||
|
.max(365, { message: "Must be less than 365" }),
|
||||||
inAppSurveyBranding: z.boolean(),
|
inAppSurveyBranding: z.boolean(),
|
||||||
linkSurveyBranding: z.boolean(),
|
linkSurveyBranding: z.boolean(),
|
||||||
placement: ZPlacement,
|
placement: ZPlacement,
|
||||||
|
|||||||
154
packages/ui/Form/index.tsx
Normal file
154
packages/ui/Form/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
|
import { Label } from "../Label";
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return <Label ref={ref} className={cn(error && "text-error", className)} htmlFor={formItemId} {...props} />;
|
||||||
|
});
|
||||||
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p ref={ref} id={formMessageId} className={cn("text-error text-sm", className)} {...props}>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
FormProvider,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, isInv
|
|||||||
className={cn(
|
className={cn(
|
||||||
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
||||||
className,
|
className,
|
||||||
isInvalid && "border border-red-600 focus:border-red-600"
|
isInvalid && "border-error focus:border-error border"
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -333,6 +333,9 @@ importers:
|
|||||||
'@formbricks/ui':
|
'@formbricks/ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/ui
|
version: link:../../packages/ui
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.4.2
|
||||||
|
version: 3.4.2(react-hook-form@7.51.4)
|
||||||
'@json2csv/node':
|
'@json2csv/node':
|
||||||
specifier: ^7.0.6
|
specifier: ^7.0.6
|
||||||
version: 7.0.6
|
version: 7.0.6
|
||||||
@@ -3429,6 +3432,14 @@ packages:
|
|||||||
tailwindcss: 3.4.3
|
tailwindcss: 3.4.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@hookform/resolvers@3.4.2(react-hook-form@7.51.4):
|
||||||
|
resolution: {integrity: sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react-hook-form: ^7.0.0
|
||||||
|
dependencies:
|
||||||
|
react-hook-form: 7.51.4(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@httptoolkit/websocket-stream@6.0.1:
|
/@httptoolkit/websocket-stream@6.0.1:
|
||||||
resolution: {integrity: sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==}
|
resolution: {integrity: sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user