mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-22 15:02:36 -06:00
Revamp No-Code-Options: You can now target a combination of conditions (e.g. a specific button on a specific page) (#691)
* feat: revamp nocode action across frontend, backend, and js lib * fix: remove console warning for ref forwarding * feat: make advancedToggle component and use it across * feat: use advancedToggle all across survey editor * feat: use advancedToggle all across survey editor for link surveys * remove: unused imports * ui tweaks * fix: form registration * chore: advancedOptionToggle now has grey box inside the div * fix: handle multiple css selectors separately * test no code demo app * replace logout with reset in demo apps --------- Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
committed by
GitHub
parent
debf8433d0
commit
317a5463e9
@@ -73,18 +73,18 @@ export default function AppPage({}) {
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-gray-300">
|
||||
On formbricks.logout() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
formbricks.logout();
|
||||
formbricks.reset();
|
||||
}}>
|
||||
Logout
|
||||
Reset
|
||||
</button>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Logout' and
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and
|
||||
try again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
202
apps/demo/pages/test-nocode-app/index.tsx
Normal file
202
apps/demo/pages/test-nocode-app/index.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import formbricks from "@formbricks/js";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import fbsetup from "../../public/fb-setup.png";
|
||||
|
||||
export default function AppPage({}) {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.body.classList.add("dark");
|
||||
} else {
|
||||
document.body.classList.remove("dark");
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
Formbricks In-product Survey Demo App
|
||||
</h1>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
This app helps you test your in-app surveys. You can create and test user actions, create and
|
||||
update user attributes, etc.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
|
||||
onClick={() => setDarkMode(!darkMode)}>
|
||||
Toggle Dark Mode
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
Copy the environment ID of your Formbricks app to the env variable in demo/.env
|
||||
</p>
|
||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
|
||||
|
||||
<div className="mt-4 flex-col items-start text-sm text-slate-700 dark:text-slate-300 sm:flex sm:items-center sm:text-base">
|
||||
<p className="mb-1 sm:mb-0 sm:mr-2">You're connected with env:</p>
|
||||
<div className="flex items-center">
|
||||
<strong className="w-32 truncate sm:w-auto">
|
||||
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
||||
</strong>
|
||||
<span className="relative ml-2 flex h-3 w-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">2. Widget Logs</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
Look at the logs to understand how the widget works.{" "}
|
||||
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
|
||||
</p>
|
||||
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
|
||||
<LogsContainer />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:grid md:grid-cols-3">
|
||||
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-gray-600 dark:bg-gray-800">
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-gray-300">
|
||||
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
formbricks.reset();
|
||||
}}>
|
||||
Reset
|
||||
</button>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and
|
||||
try again.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("Inner Text");
|
||||
}}>
|
||||
Inner Text
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text only</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
id="css-id"
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("Inner Text + CSS ID");
|
||||
}}>
|
||||
Inner Text
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text + Css ID</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("Inner Text + CSS Class");
|
||||
}}>
|
||||
Inner Text
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text + CSS Class</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
id="css-id"
|
||||
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("ID + Class");
|
||||
}}>
|
||||
ID and Class
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">ID + Class</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
id="css-id"
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("ID + Class");
|
||||
}}>
|
||||
ID only
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">ID only</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("Class only");
|
||||
}}>
|
||||
Class only
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">Class only</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
className="css-1 css-2 mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
console.log("Class + Class");
|
||||
}}>
|
||||
Class + Class
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">Class + Class</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AdvancedOptionToggle, Input } from "@formbricks/ui";
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
interface CssSelectorProps {
|
||||
isCssSelector: boolean;
|
||||
setIsCssSelector: (value: boolean) => void;
|
||||
register: UseFormRegister<any>;
|
||||
}
|
||||
|
||||
export const CssSelector = ({ isCssSelector, setIsCssSelector, register }: CssSelectorProps) => {
|
||||
return (
|
||||
<AdvancedOptionToggle
|
||||
htmlId="CssSelector"
|
||||
isChecked={isCssSelector}
|
||||
onToggle={() => {
|
||||
setIsCssSelector(!isCssSelector);
|
||||
}}
|
||||
title="CSS Selector"
|
||||
description="If a user clicks a button with a specific CSS class or id"
|
||||
childBorder={true}>
|
||||
<div className="w-full rounded-lg border border-slate-100 p-4">
|
||||
<Input
|
||||
type="text"
|
||||
className="bg-white"
|
||||
placeholder="Add .css-class or #css-id"
|
||||
{...register("noCodeConfig.cssSelector.value", { required: isCssSelector })}
|
||||
/>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AdvancedOptionToggle, Input } from "@formbricks/ui";
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
interface InnerHtmlSelectorProps {
|
||||
isInnerHtml: boolean;
|
||||
setIsInnerHtml: (value: boolean) => void;
|
||||
register: UseFormRegister<any>;
|
||||
}
|
||||
|
||||
export const InnerHtmlSelector = ({ isInnerHtml, setIsInnerHtml, register }: InnerHtmlSelectorProps) => {
|
||||
return (
|
||||
<AdvancedOptionToggle
|
||||
htmlId="InnerText"
|
||||
isChecked={isInnerHtml}
|
||||
onToggle={() => {
|
||||
setIsInnerHtml(!isInnerHtml);
|
||||
}}
|
||||
title="Inner Text"
|
||||
description="If a user clicks a button with a specific text"
|
||||
childBorder={true}>
|
||||
<div className="w-full rounded-lg border border-slate-100 p-4">
|
||||
<div className="grid grid-cols-3 gap-x-8">
|
||||
<div className="col-span-3 flex items-end">
|
||||
<Input
|
||||
type="text"
|
||||
className="bg-white"
|
||||
placeholder="e.g. 'Install App'"
|
||||
{...register("noCodeConfig.innerHtml.value", { required: isInnerHtml })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
AdvancedOptionToggle,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@formbricks/ui";
|
||||
import { Label } from "@radix-ui/react-dropdown-menu";
|
||||
import clsx from "clsx";
|
||||
import { Control, Controller, UseFormRegister } from "react-hook-form";
|
||||
|
||||
interface PageUrlSelectorProps {
|
||||
isPageUrl: boolean;
|
||||
setIsPageUrl: (value: boolean) => void;
|
||||
testUrl: string;
|
||||
setTestUrl: (value: string) => void;
|
||||
isMatch: string;
|
||||
setIsMatch: (value: string) => void;
|
||||
handleMatchClick: () => void;
|
||||
control: Control<any>;
|
||||
register: UseFormRegister<any>;
|
||||
}
|
||||
|
||||
export const PageUrlSelector = ({
|
||||
isPageUrl,
|
||||
setIsPageUrl,
|
||||
control,
|
||||
register,
|
||||
testUrl,
|
||||
isMatch,
|
||||
setIsMatch,
|
||||
setTestUrl,
|
||||
handleMatchClick,
|
||||
}: PageUrlSelectorProps) => {
|
||||
return (
|
||||
<AdvancedOptionToggle
|
||||
htmlId="PageURL"
|
||||
isChecked={isPageUrl}
|
||||
onToggle={() => {
|
||||
setIsPageUrl(!isPageUrl);
|
||||
}}
|
||||
title="Page URL"
|
||||
description="If a user visits a specific URL"
|
||||
childBorder={true}>
|
||||
<div className="col-span-1 space-y-3 p-4">
|
||||
<div className="grid grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>URL</Label>
|
||||
<Controller
|
||||
name="noCodeConfig.[pageUrl].rule"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select onValueChange={onChange} {...value} value={value}>
|
||||
<SelectTrigger className="w-[160px] bg-white">
|
||||
<SelectValue placeholder="Select match type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-white">
|
||||
<SelectItem value="exactMatch">Exactly matches</SelectItem>
|
||||
<SelectItem value="contains">Contains</SelectItem>
|
||||
<SelectItem value="startsWith">Starts with</SelectItem>
|
||||
<SelectItem value="endsWith">Ends with</SelectItem>
|
||||
<SelectItem value="notMatch">Does not exactly match</SelectItem>
|
||||
<SelectItem value="notContains">Does not contain</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-end">
|
||||
<Input
|
||||
type="text"
|
||||
className="bg-white"
|
||||
placeholder="e.g. https://app.com/dashboard"
|
||||
{...register("noCodeConfig.[pageUrl].value", { required: isPageUrl })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="text-sm text-slate-900">Test your URL</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
Enter a URL to see if a user visiting it would be tracked.
|
||||
</div>
|
||||
<div className=" rounded bg-slate-50">
|
||||
<div className="mt-1 flex">
|
||||
<Input
|
||||
type="text"
|
||||
value={testUrl}
|
||||
onChange={(e) => {
|
||||
setTestUrl(e.target.value);
|
||||
setIsMatch("default");
|
||||
}}
|
||||
className={clsx(
|
||||
isMatch === "yes"
|
||||
? "border-green-500 bg-green-50"
|
||||
: isMatch === "no"
|
||||
? "border-red-200 bg-red-50"
|
||||
: isMatch === "default"
|
||||
? "border-slate-200"
|
||||
: "bg-white"
|
||||
)}
|
||||
placeholder="e.g. https://app.com/dashboard"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="ml-2 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
handleMatchClick();
|
||||
}}>
|
||||
Test Match
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
);
|
||||
};
|
||||
@@ -2,27 +2,18 @@
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import type { NoCodeConfig } from "@formbricks/types/events";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { TActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
@@ -33,6 +24,13 @@ interface ActionSettingsTabProps {
|
||||
export default function ActionSettingsTab({ environmentId, actionClass, setOpen }: ActionSettingsTabProps) {
|
||||
const router = useRouter();
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
const [isPageUrl, setIsPageUrl] = useState(actionClass.noCodeConfig?.pageUrl ? true : false);
|
||||
const [isCssSelector, setIsCssSelector] = useState(actionClass.noCodeConfig?.cssSelector ? true : false);
|
||||
const [isInnerHtml, setIsInnerHtml] = useState(actionClass.noCodeConfig?.innerHtml ? true : false);
|
||||
const [isUpdatingAction, setIsUpdatingAction] = useState(false);
|
||||
const [isDeletingAction, setIsDeletingAction] = useState(false);
|
||||
|
||||
const { register, handleSubmit, control, watch } = useForm({
|
||||
defaultValues: {
|
||||
@@ -41,36 +39,24 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
noCodeConfig: actionClass.noCodeConfig,
|
||||
},
|
||||
});
|
||||
const [isUpdatingAction, setIsUpdatingAction] = useState(false);
|
||||
const [isDeletingAction, setIsDeletingAction] = useState(false);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
const filteredNoCodeConfig: TActionClassNoCodeConfig = {};
|
||||
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
if (isPageUrl && pageUrl?.rule && pageUrl?.value) {
|
||||
filteredNoCodeConfig.pageUrl = { rule: pageUrl.rule, value: pageUrl.value };
|
||||
}
|
||||
if (isInnerHtml && innerHtml?.value) {
|
||||
filteredNoCodeConfig.innerHtml = { value: innerHtml.value };
|
||||
}
|
||||
if (isCssSelector && cssSelector?.value) {
|
||||
filteredNoCodeConfig.cssSelector = { value: cssSelector.value };
|
||||
}
|
||||
|
||||
setIsUpdatingAction(true);
|
||||
await updateActionClass(environmentId, actionClass.id, updatedData);
|
||||
router.refresh();
|
||||
setIsUpdatingAction(false);
|
||||
setOpen(false);
|
||||
return filteredNoCodeConfig;
|
||||
};
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: NoCodeConfig): NoCodeConfig => {
|
||||
const { type } = noCodeConfig;
|
||||
return {
|
||||
type,
|
||||
[type]: noCodeConfig[type],
|
||||
};
|
||||
};
|
||||
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
|
||||
const handleMatchClick = () => {
|
||||
const match = testURLmatch(
|
||||
testUrl,
|
||||
@@ -82,6 +68,29 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
if (match === "no") toast.error("Your survey would not be shown.");
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
setIsUpdatingAction(true);
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
if (!isPageUrl && !isCssSelector && !isInnerHtml) throw new Error("Please select atleast one selector");
|
||||
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
await updateActionClass(environmentId, actionClass.id, updatedData);
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success("Action updated successfully");
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setIsUpdatingAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
@@ -99,181 +108,64 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
return (
|
||||
<div>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="">
|
||||
<Label className="text-slate-600">Display name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. Product Team Info"
|
||||
{...register("name", {
|
||||
value: actionClass.name,
|
||||
disabled: actionClass.type === "automatic" || actionClass.type === "code" ? true : false,
|
||||
})}
|
||||
/>
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>What did your user do?</Label>
|
||||
<Input
|
||||
placeholder="E.g. Clicked Download"
|
||||
{...register("name", {
|
||||
value: actionClass.name,
|
||||
disabled: actionClass.type === "automatic" || actionClass.type === "code" ? true : false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
placeholder="User clicked Download Button "
|
||||
{...register("description", {
|
||||
value: actionClass.description,
|
||||
disabled: actionClass.type === "automatic" ? true : false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<Label className="text-slate-600">Display description</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. Triggers when user changed subscription"
|
||||
{...register("description", {
|
||||
value: actionClass.description,
|
||||
disabled: actionClass.type === "automatic" ? true : false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<Label>Action Type</Label>
|
||||
{actionClass.type === "code" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This is a code action. Please make changes in your code base.
|
||||
</p>
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="w-full space-y-4">
|
||||
<Controller
|
||||
name="noCodeConfig.type"
|
||||
defaultValue={"pageUrl"}
|
||||
control={control}
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<RadioGroup className="flex" onValueChange={onChange} onBlur={onBlur} value={value}>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="pageUrl" id="pageUrl" className="bg-slate-50" />
|
||||
<Label htmlFor="pageUrl" className="flex cursor-pointer items-center">
|
||||
Page URL
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="innerHtml" id="innerHtml" className="bg-slate-50" />
|
||||
<Label htmlFor="innerHtml" className="flex cursor-pointer items-center">
|
||||
Inner Text
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="cssSelector" id="cssSelector" className="bg-slate-50" />
|
||||
<Label htmlFor="cssSelector" className="flex cursor-pointer items-center">
|
||||
CSS Selector
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
{(watch("noCodeConfig.type") === "pageUrl" || !watch("noCodeConfig.type")) && (
|
||||
<>
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>URL</Label>
|
||||
<Controller
|
||||
name="noCodeConfig.pageUrl.rule"
|
||||
defaultValue={"exactMatch"}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
/* onValueChange={(e) => {
|
||||
setMatchType(e as MatchType);
|
||||
setIsMatch("default");
|
||||
}} */
|
||||
{...field}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select match type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="exactMatch">Exactly matches</SelectItem>
|
||||
<SelectItem value="contains">Contains</SelectItem>
|
||||
<SelectItem value="startsWith">Starts with</SelectItem>
|
||||
<SelectItem value="endsWith">Ends with</SelectItem>
|
||||
<SelectItem value="notMatch">Does not exactly match</SelectItem>
|
||||
<SelectItem value="notContains">Does not contain</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. https://app.formbricks.com/dashboard"
|
||||
{...register("noCodeConfig.[pageUrl].value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">Test Your URL</Label>
|
||||
<div className=" rounded bg-slate-50 p-4">
|
||||
<Label className="font-normal text-slate-500">
|
||||
Enter a URL to see if it matches your event URL
|
||||
</Label>
|
||||
<div className="mt-1 flex">
|
||||
<Input
|
||||
type="text"
|
||||
value={testUrl}
|
||||
onChange={(e) => {
|
||||
setTestUrl(e.target.value);
|
||||
setIsMatch("default");
|
||||
}}
|
||||
className={clsx(
|
||||
isMatch === "yes"
|
||||
? "border-green-500 bg-green-50"
|
||||
: isMatch === "no"
|
||||
? "border-red-200 bg-red-50"
|
||||
: isMatch === "default"
|
||||
? "border-slate-200 bg-white"
|
||||
: null
|
||||
)}
|
||||
placeholder="Paste the URL you want the event to trigger on"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="ml-2 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
handleMatchClick();
|
||||
}}>
|
||||
Test Match
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{watch("noCodeConfig.type") === "innerHtml" && (
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>Inner Text</Label>
|
||||
</div>
|
||||
<div className="col-span-3 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. 'Install App'"
|
||||
{...register("noCodeConfig.innerHtml.value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{watch("noCodeConfig.type") === "cssSelector" && (
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>CSS Tag</Label>
|
||||
</div>
|
||||
<div className="col-span-3 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. #install-button"
|
||||
{...register("noCodeConfig.cssSelector.value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{actionClass.type === "code" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This is a code action. Please make changes in your code base.
|
||||
</p>
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<>
|
||||
<div>
|
||||
<Label>Select By</Label>
|
||||
</div>
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This action was created automatically. You cannot make changes to it.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<CssSelector
|
||||
isCssSelector={isCssSelector}
|
||||
setIsCssSelector={setIsCssSelector}
|
||||
register={register}
|
||||
/>
|
||||
<PageUrlSelector
|
||||
isPageUrl={isPageUrl}
|
||||
setIsPageUrl={setIsPageUrl}
|
||||
register={register}
|
||||
control={control}
|
||||
testUrl={testUrl}
|
||||
setTestUrl={setTestUrl}
|
||||
isMatch={isMatch}
|
||||
setIsMatch={setIsMatch}
|
||||
handleMatchClick={handleMatchClick}
|
||||
/>
|
||||
<InnerHtmlSelector
|
||||
isInnerHtml={isInnerHtml}
|
||||
setIsInnerHtml={setIsInnerHtml}
|
||||
register={register}
|
||||
/>
|
||||
</>
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This action was created automatically. You cannot make changes to it.
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex justify-between border-t border-slate-200 py-6">
|
||||
<div>
|
||||
{actionClass.type !== "automatic" && (
|
||||
@@ -287,7 +179,7 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="secondary" href="https://formbricks.com/docs" target="_blank">
|
||||
<Button variant="secondary" href="https://formbricks.com/docs/actions/no-code" target="_blank">
|
||||
Read Docs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { createActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector";
|
||||
|
||||
interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
@@ -32,40 +23,30 @@ interface AddNoCodeActionModalProps {
|
||||
export default function AddNoCodeActionModal({ environmentId, open, setOpen }: AddNoCodeActionModalProps) {
|
||||
const router = useRouter();
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
|
||||
// clean up noCodeConfig before submitting by removing unnecessary fields
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { type } = noCodeConfig;
|
||||
return {
|
||||
type,
|
||||
[type]: noCodeConfig[type],
|
||||
};
|
||||
};
|
||||
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
|
||||
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
|
||||
try {
|
||||
await createActionClass(environmentId, updatedData);
|
||||
router.refresh();
|
||||
reset();
|
||||
setOpen(false);
|
||||
toast.success("Action added successfully.");
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const [isPageUrl, setIsPageUrl] = useState(false);
|
||||
const [isCssSelector, setIsCssSelector] = useState(false);
|
||||
const [isInnerHtml, setIsInnerText] = useState(false);
|
||||
const [isCreatingAction, setIsCreatingAction] = useState(false);
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
const filteredNoCodeConfig: TActionClassNoCodeConfig = {};
|
||||
|
||||
if (isPageUrl && pageUrl?.rule && pageUrl?.value) {
|
||||
filteredNoCodeConfig.pageUrl = { rule: pageUrl.rule, value: pageUrl.value };
|
||||
}
|
||||
if (isInnerHtml && innerHtml?.value) {
|
||||
filteredNoCodeConfig.innerHtml = { value: innerHtml.value };
|
||||
}
|
||||
if (isCssSelector && cssSelector?.value) {
|
||||
filteredNoCodeConfig.cssSelector = { value: cssSelector.value };
|
||||
}
|
||||
|
||||
return filteredNoCodeConfig;
|
||||
};
|
||||
|
||||
const handleMatchClick = () => {
|
||||
const match = testURLmatch(
|
||||
testUrl,
|
||||
@@ -77,8 +58,47 @@ export default function AddNoCodeActionModal({ environmentId, open, setOpen }: A
|
||||
if (match === "no") toast.error("Your survey would not be shown.");
|
||||
};
|
||||
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
try {
|
||||
setIsCreatingAction(true);
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
if (!isPageUrl && !isCssSelector && !isInnerHtml) throw new Error("Please select atleast one selector");
|
||||
|
||||
if (isPageUrl && data.noCodeConfig?.pageUrl?.rule === undefined) {
|
||||
throw new Error("Please select a rule for page URL");
|
||||
}
|
||||
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
|
||||
await createActionClass(environmentId, updatedData);
|
||||
router.refresh();
|
||||
reset();
|
||||
resetAllStates(false);
|
||||
toast.success("Action added successfully.");
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setIsCreatingAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetAllStates = (open: boolean) => {
|
||||
setIsCssSelector(false);
|
||||
setIsPageUrl(false);
|
||||
setIsInnerText(false);
|
||||
setTestUrl("");
|
||||
setIsMatch("");
|
||||
reset();
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={false}>
|
||||
<Modal open={open} setOpen={() => resetAllStates(false)} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center justify-between p-6">
|
||||
@@ -87,9 +107,9 @@ export default function AddNoCodeActionModal({ environmentId, open, setOpen }: A
|
||||
<CursorArrowRaysIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Add Action</div>
|
||||
<div className="text-xl font-medium text-slate-700">Track New User Action</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Track a user action to display surveys when it's performed.
|
||||
Track a user action to display surveys or create user segment.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,169 +118,49 @@ export default function AddNoCodeActionModal({ environmentId, open, setOpen }: A
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div>
|
||||
<Label>Select By</Label>
|
||||
<Controller
|
||||
name="noCodeConfig.type"
|
||||
defaultValue={"pageUrl"}
|
||||
control={control}
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<RadioGroup className="flex" onValueChange={onChange} onBlur={onBlur} value={value}>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="pageUrl" id="pageUrl" className="bg-slate-50" />
|
||||
<Label htmlFor="pageUrl" className="flex cursor-pointer items-center">
|
||||
Page URL
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="innerHtml" id="innerHtml" className="bg-slate-50" />
|
||||
<Label htmlFor="innerHtml" className="flex cursor-pointer items-center">
|
||||
Inner Text
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3">
|
||||
<RadioGroupItem value="cssSelector" id="cssSelector" className="bg-slate-50" />
|
||||
<Label htmlFor="cssSelector" className="flex cursor-pointer items-center">
|
||||
CSS Selector
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-x-8">
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>Name</Label>
|
||||
<Input placeholder="e.g. Dashboard Page View" {...register("name", { required: true })} />
|
||||
<Label>What did your user do?</Label>
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
<Input placeholder="e.g. User visited dashboard" {...register("description")} />
|
||||
<Input placeholder="User clicked Download Button " {...register("description")} />
|
||||
</div>
|
||||
</div>
|
||||
{(watch("noCodeConfig.type") === "pageUrl" || !watch("noCodeConfig.type")) && (
|
||||
<>
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>URL</Label>
|
||||
<Controller
|
||||
name="noCodeConfig.pageUrl.rule"
|
||||
defaultValue={"exactMatch"}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
/* onValueChange={(e) => {
|
||||
setMatchType(e as MatchType);
|
||||
setIsMatch("default");
|
||||
}} */
|
||||
{...field}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select match type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="exactMatch">Exactly matches</SelectItem>
|
||||
<SelectItem value="contains">Contains</SelectItem>
|
||||
<SelectItem value="startsWith">Starts with</SelectItem>
|
||||
<SelectItem value="endsWith">Ends with</SelectItem>
|
||||
<SelectItem value="notMatch">Does not exactly match</SelectItem>
|
||||
<SelectItem value="notContains">Does not contain</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. https://app.formbricks.com/dashboard"
|
||||
{...register("noCodeConfig.[pageUrl].value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">Test Your URL</Label>
|
||||
<div className=" rounded bg-slate-50 p-4">
|
||||
<Label className="font-normal text-slate-500">
|
||||
Enter a URL to see if it matches your event URL
|
||||
</Label>
|
||||
<div className="mt-1 flex">
|
||||
<Input
|
||||
type="text"
|
||||
value={testUrl}
|
||||
onChange={(e) => {
|
||||
setTestUrl(e.target.value);
|
||||
setIsMatch("default");
|
||||
}}
|
||||
className={clsx(
|
||||
isMatch === "yes"
|
||||
? "border-green-500 bg-green-50"
|
||||
: isMatch === "no"
|
||||
? "border-red-200 bg-red-50"
|
||||
: isMatch === "default"
|
||||
? "border-slate-200 bg-white"
|
||||
: null
|
||||
)}
|
||||
placeholder="Paste the URL you want the event to trigger on"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="ml-2 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
handleMatchClick();
|
||||
}}>
|
||||
Test Match
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{watch("noCodeConfig.type") === "innerHtml" && (
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>Inner Text</Label>
|
||||
</div>
|
||||
<div className="col-span-3 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. 'Install App'"
|
||||
{...register("noCodeConfig.innerHtml.value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{watch("noCodeConfig.type") === "cssSelector" && (
|
||||
<div className="grid w-full grid-cols-3 gap-x-8">
|
||||
<div className="col-span-1">
|
||||
<Label>CSS Tag</Label>
|
||||
</div>
|
||||
<div className="col-span-3 flex w-full items-end">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. #install-button"
|
||||
{...register("noCodeConfig.cssSelector.value", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>Select By</Label>
|
||||
</div>
|
||||
<CssSelector
|
||||
isCssSelector={isCssSelector}
|
||||
setIsCssSelector={setIsCssSelector}
|
||||
register={register}
|
||||
/>
|
||||
<PageUrlSelector
|
||||
isPageUrl={isPageUrl}
|
||||
setIsPageUrl={setIsPageUrl}
|
||||
register={register}
|
||||
control={control}
|
||||
testUrl={testUrl}
|
||||
setTestUrl={setTestUrl}
|
||||
isMatch={isMatch}
|
||||
setIsMatch={setIsMatch}
|
||||
handleMatchClick={handleMatchClick}
|
||||
/>
|
||||
<InnerHtmlSelector
|
||||
isInnerHtml={isInnerHtml}
|
||||
setIsInnerHtml={setIsInnerText}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}>
|
||||
<Button type="button" variant="minimal" onClick={() => resetAllStates(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit">
|
||||
Add Action
|
||||
<Button variant="darkCTA" type="submit" loading={isCreatingAction}>
|
||||
Track Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { Badge, Input, Label, RadioGroup, RadioGroupItem, Switch } from "@formbricks/ui";
|
||||
import { AdvancedOptionToggle, Badge, Input, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import Link from "next/link";
|
||||
@@ -142,83 +142,79 @@ export default function RecontactOptionsCard({
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="recontactDays" checked={ignoreWaiting} onCheckedChange={handleCheckMark} />
|
||||
{/* <Checkbox id="recontactDays" checked={ignoreWaiting} onCheckedChange={handleCheckMark} /> */}
|
||||
<Label htmlFor="recontactDays" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Ignore waiting time between surveys</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
This setting overwrites your{" "}
|
||||
<Link
|
||||
className="decoration-brand-dark underline"
|
||||
href={`/environments/${environmentId}/settings/product`}
|
||||
target="_blank">
|
||||
waiting period
|
||||
</Link>
|
||||
. Use with caution.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{ignoreWaiting && localSurvey.recontactDays !== null && (
|
||||
<div className="p-3">
|
||||
<RadioGroup
|
||||
value={localSurvey.recontactDays.toString()}
|
||||
className="flex flex-col space-y-3"
|
||||
onValueChange={(v) => {
|
||||
const updatedSurvey = { ...localSurvey, recontactDays: v === "null" ? null : Number(v) };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}>
|
||||
<Label
|
||||
htmlFor="ignore"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<RadioGroupItem
|
||||
value="0"
|
||||
id="ignore"
|
||||
className="aria-checked:border-brand-dark mx-5 text-sm disabled:border-slate-400 aria-checked:border-2"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-700">Always show survey</p>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="recontactDays"
|
||||
isChecked={ignoreWaiting}
|
||||
onToggle={handleCheckMark}
|
||||
title="Ignore waiting time between surveys"
|
||||
childBorder={false}
|
||||
description={
|
||||
<>
|
||||
This setting overwrites your{" "}
|
||||
<Link
|
||||
className="decoration-brand-dark underline"
|
||||
href={`/environments/${environmentId}/settings/product`}
|
||||
target="_blank">
|
||||
waiting period
|
||||
</Link>
|
||||
. Use with caution.
|
||||
</>
|
||||
}>
|
||||
{localSurvey.recontactDays !== null && (
|
||||
<RadioGroup
|
||||
value={localSurvey.recontactDays.toString()}
|
||||
className="flex w-full flex-col space-y-3 bg-white"
|
||||
onValueChange={(v) => {
|
||||
const updatedSurvey = { ...localSurvey, recontactDays: v === "null" ? null : Number(v) };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}>
|
||||
<Label
|
||||
htmlFor="ignore"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<RadioGroupItem
|
||||
value="0"
|
||||
id="ignore"
|
||||
className="aria-checked:border-brand-dark mx-4 text-sm disabled:border-slate-400 aria-checked:border-2"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-700">Always show survey</p>
|
||||
|
||||
<p className="mt-2 text-xs font-normal text-slate-600">
|
||||
When conditions match, waiting time will be ignored and survey shown.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
<p className="mt-2 text-xs font-normal text-slate-600">
|
||||
When conditions match, waiting time will be ignored and survey shown.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<label
|
||||
htmlFor="newDays"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<RadioGroupItem
|
||||
value={inputDays === 0 ? "1" : inputDays.toString()} //Fixes that both radio buttons are checked when inputDays is 0
|
||||
id="newDays"
|
||||
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
|
||||
/>
|
||||
<div className="">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Wait
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
id="inputDays"
|
||||
value={inputDays === 0 ? 1 : inputDays}
|
||||
onChange={handleRecontactDaysChange}
|
||||
className="ml-2 mr-2 inline w-16 text-center text-sm"
|
||||
/>
|
||||
days before showing this survey again.
|
||||
</p>
|
||||
<label
|
||||
htmlFor="newDays"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<RadioGroupItem
|
||||
value={inputDays === 0 ? "1" : inputDays.toString()} //Fixes that both radio buttons are checked when inputDays is 0
|
||||
id="newDays"
|
||||
className="aria-checked:border-brand-dark mx-4 disabled:border-slate-400 aria-checked:border-2"
|
||||
/>
|
||||
<div className="">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Wait
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
id="inputDays"
|
||||
value={inputDays === 0 ? 1 : inputDays}
|
||||
onChange={handleRecontactDaysChange}
|
||||
className="ml-2 mr-2 inline w-16 text-center text-sm"
|
||||
/>
|
||||
days before showing this survey again.
|
||||
</p>
|
||||
|
||||
<p className="mt-2 text-xs font-normal text-slate-600">
|
||||
Overwrites waiting period between surveys to {inputDays === 0 ? 1 : inputDays} day(s).
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<p className="mt-2 text-xs font-normal text-slate-600">
|
||||
Overwrites waiting period between surveys to {inputDays === 0 ? 1 : inputDays} day(s).
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { DatePicker, Input, Label, Switch } from "@formbricks/ui";
|
||||
import { AdvancedOptionToggle, DatePicker, Input, Label } from "@formbricks/ui";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -159,162 +159,104 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
{/* Close Survey on Limit */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="autoComplete" checked={autoComplete} onCheckedChange={handleCheckMark} />
|
||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Close survey on response limit</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Automatically close the survey after a certain number of responses.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{autoComplete && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<label
|
||||
htmlFor="autoCompleteResponses"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div className="">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Automatically mark the survey as complete after
|
||||
<Input
|
||||
autoFocus
|
||||
type="number"
|
||||
min={
|
||||
localSurvey?._count?.responses
|
||||
? (localSurvey?._count?.responses + 1).toString()
|
||||
: "1"
|
||||
}
|
||||
id="autoCompleteResponses"
|
||||
value={localSurvey.autoComplete?.toString()}
|
||||
onChange={handleInputResponse}
|
||||
onBlur={handleInputResponseBlur}
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
completed responses.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="closeOnNumberOfResponse"
|
||||
isChecked={autoComplete}
|
||||
onToggle={handleCheckMark}
|
||||
title="Close survey on response limit"
|
||||
description="Automatically close the survey after a certain number of responses."
|
||||
childBorder={true}>
|
||||
<label htmlFor="autoCompleteResponses" className="cursor-pointer bg-slate-50 p-4">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Automatically mark the survey as complete after
|
||||
<Input
|
||||
autoFocus
|
||||
type="number"
|
||||
min={localSurvey?._count?.responses ? (localSurvey?._count?.responses + 1).toString() : "1"}
|
||||
id="autoCompleteResponses"
|
||||
value={localSurvey.autoComplete?.toString()}
|
||||
onChange={handleInputResponse}
|
||||
onBlur={handleInputResponseBlur}
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
completed responses.
|
||||
</p>
|
||||
</label>
|
||||
</AdvancedOptionToggle>
|
||||
{/* Close Survey on Date */}
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="surveyDeadline"
|
||||
checked={surveyCloseOnDateToggle}
|
||||
onCheckedChange={handleSurveyCloseOnDateToggle}
|
||||
/>
|
||||
<Label htmlFor="surveyDeadline" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Close survey on date</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Automatically closes the survey at the beginning of the day (UTC).
|
||||
</p>
|
||||
{localSurvey.status === "completed" && (
|
||||
<p className="text-xs font-normal text-slate-500">This form is already completed.</p>
|
||||
)}
|
||||
</div>
|
||||
</Label>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="closeOnDate"
|
||||
isChecked={surveyCloseOnDateToggle}
|
||||
onToggle={handleSurveyCloseOnDateToggle}
|
||||
title="Close survey on date"
|
||||
description="Automatically closes the survey at the beginning of the day (UTC)."
|
||||
childBorder={true}>
|
||||
<div className="flex cursor-pointer p-4">
|
||||
<p className="mr-2 mt-3 text-sm font-semibold text-slate-700">
|
||||
Automatically mark survey as complete on:
|
||||
</p>
|
||||
<DatePicker date={closeOnDate} handleDateChange={handleCloseOnDateChange} />
|
||||
</div>
|
||||
{surveyCloseOnDateToggle && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<div className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<p className="mr-2 text-sm font-semibold text-slate-700">
|
||||
Automatically mark survey as complete on:
|
||||
</p>
|
||||
<DatePicker date={closeOnDate} handleDateChange={handleCloseOnDateChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
|
||||
{/* Redirect on completion */}
|
||||
{localSurvey.type === "link" && (
|
||||
<>
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="redirectUrl"
|
||||
checked={redirectToggle}
|
||||
onCheckedChange={handleRedirectCheckMark}
|
||||
/>
|
||||
<Label htmlFor="redirectUrl" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Redirect on completion</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Redirect user to specified link on survey completion
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{redirectToggle && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<div className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<p className="mr-2 whitespace-nowrap text-sm font-semibold text-slate-700">
|
||||
Redirect respondents here:
|
||||
</p>
|
||||
<Input
|
||||
autoFocus
|
||||
className="bg-white"
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="redirectUrl"
|
||||
isChecked={redirectToggle}
|
||||
onToggle={handleRedirectCheckMark}
|
||||
title="Redirect on completion"
|
||||
description="Redirect user to specified link on survey completion"
|
||||
childBorder={true}>
|
||||
<div className="w-full p-4">
|
||||
<div className="flex w-full cursor-pointer items-center">
|
||||
<p className="mr-2 w-[400px] text-sm font-semibold text-slate-700">
|
||||
Redirect respondents here:
|
||||
</p>
|
||||
<Input
|
||||
autoFocus
|
||||
className="w-full bg-white"
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
|
||||
{/* Adjust Survey Closed Message */}
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="adjustSurveyClosedMessage"
|
||||
checked={surveyClosedMessageToggle}
|
||||
onCheckedChange={handleCloseSurveyMessageToggle}
|
||||
/>
|
||||
<Label htmlFor="adjustSurveyClosedMessage" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">
|
||||
{" "}
|
||||
{"Adjust 'Survey Closed' message"}
|
||||
</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Change the message visitors see when the survey is closed.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{surveyClosedMessageToggle && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<div className="w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<Label htmlFor="headline">Heading</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="heading"
|
||||
defaultValue={surveyClosedMessage.heading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
|
||||
/>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="adjustSurveyClosedMessage"
|
||||
isChecked={surveyClosedMessageToggle}
|
||||
onToggle={handleCloseSurveyMessageToggle}
|
||||
title="Adjust 'Survey Closed' message"
|
||||
description="Change the message visitors see when the survey is closed."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="headline">Heading</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="heading"
|
||||
defaultValue={surveyClosedMessage.heading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
|
||||
/>
|
||||
|
||||
<Label htmlFor="headline">Subheading</Label>
|
||||
<Input
|
||||
className="mt-2 bg-white"
|
||||
id="subheading"
|
||||
name="subheading"
|
||||
defaultValue={surveyClosedMessage.subheading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ subheading: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Label htmlFor="headline">Subheading</Label>
|
||||
<Input
|
||||
className="mt-2 bg-white"
|
||||
id="subheading"
|
||||
name="subheading"
|
||||
defaultValue={surveyClosedMessage.subheading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ subheading: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,17 +6,16 @@ import { useEventClasses } from "@/lib/eventClasses/eventClasses";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import {
|
||||
AdvancedOptionToggle,
|
||||
Badge,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
} from "@formbricks/ui";
|
||||
import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
@@ -222,36 +221,28 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-2 flex items-center space-x-1 p-4">
|
||||
<Switch id="autoClose" checked={autoClose} onCheckedChange={handleCheckMark} />
|
||||
<Label htmlFor="autoClose" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Auto close on inactivity</h3>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{autoClose && (
|
||||
<div className="ml-2 flex items-center space-x-1 px-4 pb-4">
|
||||
<label
|
||||
htmlFor="autoCloseSeconds"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div className="">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Automatically close survey after
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
id="autoCloseSeconds"
|
||||
value={localSurvey.autoClose?.toString()}
|
||||
onChange={(e) => handleInputSeconds(e)}
|
||||
className="ml-2 mr-2 inline w-16 text-center text-sm"
|
||||
/>
|
||||
seconds with no initial interaction.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<AdvancedOptionToggle
|
||||
htmlId="autoClose"
|
||||
isChecked={autoClose}
|
||||
onToggle={handleCheckMark}
|
||||
title="Auto close on inactivity"
|
||||
description="Automatically close the survey if the user does not respond after certain number of seconds"
|
||||
childBorder={true}>
|
||||
<label htmlFor="autoCloseSeconds" className="cursor-pointer p-4">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Automatically close survey after
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
id="autoCloseSeconds"
|
||||
value={localSurvey.autoClose?.toString()}
|
||||
onChange={(e) => handleInputSeconds(e)}
|
||||
className="mx-2 inline w-16 bg-white text-center text-sm"
|
||||
/>
|
||||
seconds with no initial interaction.
|
||||
</p>
|
||||
</label>
|
||||
</AdvancedOptionToggle>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
<AddNoCodeActionModal
|
||||
|
||||
@@ -16,9 +16,10 @@ export const checkPageUrl = async (): Promise<Result<void, InvalidMatchTypeError
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
const pageUrlEvents: TActionClass[] = state?.noCodeActionClasses.filter(
|
||||
(e) => e.noCodeConfig?.type === "pageUrl"
|
||||
);
|
||||
const pageUrlEvents: TActionClass[] = (state?.noCodeActionClasses || []).filter((action) => {
|
||||
const { innerHtml, cssSelector, pageUrl } = action.noCodeConfig || {};
|
||||
return pageUrl && !innerHtml && !cssSelector;
|
||||
});
|
||||
|
||||
if (pageUrlEvents.length === 0) {
|
||||
return okVoid();
|
||||
@@ -101,43 +102,45 @@ export const checkClickMatch = (event: MouseEvent) => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const innerHtmlEvents: TActionClass[] = state?.noCodeActionClasses?.filter(
|
||||
(e) => e.noCodeConfig?.type === "innerHtml"
|
||||
);
|
||||
const cssSelectorEvents: TActionClass[] = state?.noCodeActionClasses?.filter(
|
||||
(e) => e.noCodeConfig?.type === "cssSelector"
|
||||
);
|
||||
|
||||
const targetElement = event.target as HTMLElement;
|
||||
(state?.noCodeActionClasses || []).forEach((action: TActionClass) => {
|
||||
const innerHtml = action.noCodeConfig?.innerHtml?.value;
|
||||
const cssSelectors = action.noCodeConfig?.cssSelector?.value;
|
||||
const pageUrl = action.noCodeConfig?.pageUrl?.value;
|
||||
|
||||
innerHtmlEvents.forEach((e) => {
|
||||
const innerHtml = e.noCodeConfig?.innerHtml;
|
||||
if (innerHtml && targetElement.innerHTML === innerHtml.value) {
|
||||
trackAction(e.name).then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value) => {},
|
||||
(err) => {
|
||||
errorHandler.handle(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
if (!innerHtml && !cssSelectors && !pageUrl) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
cssSelectorEvents.forEach((e) => {
|
||||
const cssSelector = e.noCodeConfig?.cssSelector;
|
||||
if (cssSelector && targetElement.matches(cssSelector.value)) {
|
||||
trackAction(e.name).then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value) => {},
|
||||
(err) => {
|
||||
errorHandler.handle(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
if (innerHtml && targetElement.innerHTML !== innerHtml) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cssSelectors) {
|
||||
// Split selectors that start with a . or # including the . or #
|
||||
const individualSelectors = cssSelectors.split(/\s*(?=[.#])/);
|
||||
for (let selector of individualSelectors) {
|
||||
if (!targetElement.matches(selector)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pageUrl) {
|
||||
const urlMatch = checkUrlMatch(window.location.href, pageUrl, action.noCodeConfig?.pageUrl?.rule);
|
||||
if (!urlMatch.ok || !urlMatch.value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
trackAction(action.name).then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value) => {},
|
||||
(err) => {
|
||||
errorHandler.handle(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZActionClassNoCodeConfig = z.object({
|
||||
type: z.union([z.literal("innerHtml"), z.literal("pageUrl"), z.literal("cssSelector")]),
|
||||
// The "type field has been made optional to allow for multiple selectors in one noCode action from now on
|
||||
// Use the existence check of the fields to determine the types of the noCode action
|
||||
type: z.optional(z.union([z.literal("innerHtml"), z.literal("pageUrl"), z.literal("cssSelector")])),
|
||||
pageUrl: z.optional(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
|
||||
44
packages/ui/components/AdvancedOptionToggle.tsx
Normal file
44
packages/ui/components/AdvancedOptionToggle.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Label } from "./Label";
|
||||
import { Switch } from "./Switch";
|
||||
|
||||
interface AdvancedOptionToggleProps {
|
||||
isChecked: boolean;
|
||||
onToggle: (checked: boolean) => void;
|
||||
htmlId: string;
|
||||
title: string;
|
||||
description: any;
|
||||
children: React.ReactNode;
|
||||
childBorder?: boolean;
|
||||
}
|
||||
|
||||
export function AdvancedOptionToggle({
|
||||
isChecked,
|
||||
onToggle,
|
||||
htmlId,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
childBorder,
|
||||
}: AdvancedOptionToggleProps) {
|
||||
return (
|
||||
<div className="px-4 py-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id={htmlId} checked={isChecked} onCheckedChange={onToggle} />
|
||||
<Label htmlFor={htmlId} className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">{title}</h3>
|
||||
<p className="text-xs font-normal text-slate-500">{description}</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{isChecked && (
|
||||
<div
|
||||
className={`mt-4 flex w-full items-center space-x-1 rounded-lg ${
|
||||
childBorder ? "border" : ""
|
||||
} bg-slate-50`}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { AdvancedOptionToggle } from "./components/AdvancedOptionToggle";
|
||||
export { Alert, AlertDescription, AlertTitle } from "./components/Alert";
|
||||
export { PersonAvatar, ProfileAvatar } from "./components/Avatars";
|
||||
export { Badge } from "./components/Badge";
|
||||
|
||||
Reference in New Issue
Block a user