- {eventClass.type === "code" ? (
+ {actionClass.type === "code" ? (
This is a code action. Please make changes in your code base.
- ) : eventClass.type === "noCode" ? (
+ ) : actionClass.type === "noCode" ? (
- ) : eventClass.type === "automatic" ? (
+ ) : actionClass.type === "automatic" ? (
This action was created automatically. You cannot make changes to it.
@@ -266,7 +261,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
- {eventClass.type !== "automatic" && (
+ {actionClass.type !== "automatic" && (
- {eventClass.type !== "automatic" && (
+ {actionClass.type !== "automatic" && (
-
@@ -298,8 +293,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
onDelete={async () => {
setOpen(false);
try {
- await deleteEventClass(environmentId, eventClass.id);
- mutateEventClasses();
+ await deleteActionClass(environmentId, actionClass.id);
+ router.refresh();
toast.success("Action deleted successfully");
} catch (error) {
toast.error("Something went wrong. Please try again.");
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading.tsx
new file mode 100644
index 00000000000..758fe7d8410
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading.tsx
@@ -0,0 +1,11 @@
+export default function ActionTableHeading() {
+ return (
+ <>
+
+
Edit
+
User Actions
+
Created
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/events/AddNoCodeEventModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx
similarity index 93%
rename from apps/web/app/(app)/environments/[environmentId]/events/AddNoCodeEventModal.tsx
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx
index cd4df584c4b..28055b32d83 100644
--- a/apps/web/app/(app)/environments/[environmentId]/events/AddNoCodeEventModal.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx
@@ -1,8 +1,6 @@
"use client";
import Modal from "@/components/shared/Modal";
-import { createEventClass } from "@/lib/eventClasses/eventClasses";
-import type { Event, NoCodeConfig } from "@formbricks/types/events";
import {
Button,
Input,
@@ -21,24 +19,22 @@ import { useState } from "react";
import { Controller, 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";
-interface EventDetailModalProps {
+interface AddNoCodeActionModalProps {
environmentId: string;
open: boolean;
setOpen: (v: boolean) => void;
- mutateEventClasses: (data?: any) => void;
}
-export default function AddNoCodeEventModal({
- environmentId,
- open,
- setOpen,
- mutateEventClasses,
-}: EventDetailModalProps) {
+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: NoCodeConfig): NoCodeConfig => {
+ const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
const { type } = noCodeConfig;
return {
type,
@@ -46,18 +42,18 @@ export default function AddNoCodeEventModal({
};
};
- const submitEventClass = async (data: Partial
): Promise => {
- const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
+ const submitEventClass = async (data: Partial): Promise => {
+ const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
- const updatedData: Event = {
+ const updatedData: TActionClassInput = {
...data,
noCodeConfig: filteredNoCodeConfig,
type: "noCode",
- } as Event;
+ } as TActionClassInput;
try {
- await createEventClass(environmentId, updatedData);
- mutateEventClasses();
+ await createActionClass(environmentId, updatedData);
+ router.refresh();
reset();
setOpen(false);
toast.success("Action added successfully.");
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/layout.tsx
new file mode 100644
index 00000000000..313ea9822c3
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/layout.tsx
@@ -0,0 +1,11 @@
+import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
+import ContentWrapper from "@/components/shared/ContentWrapper";
+
+export default function ActionsAndAttributesLayout({ params, children }) {
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/loading.tsx
new file mode 100644
index 00000000000..aa6f89bbef2
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/loading.tsx
@@ -0,0 +1,47 @@
+import { Button } from "@formbricks/ui";
+import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
+
+export default function Loading() {
+ return (
+ <>
+
+
+
+ Loading
+
+
+
+
+
+
Edit
+
User Actions
+
Created
+
+
+
+ {[...Array(3)].map((_, index) => (
+
+ ))}
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx
new file mode 100644
index 00000000000..4d4c789201e
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx
@@ -0,0 +1,23 @@
+import ActionClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable";
+import ActionClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData";
+import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading";
+import { getActionClasses } from "@formbricks/lib/services/actionClass";
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Actions",
+};
+
+export default async function ActionClassesComponent({ params }) {
+ let actionClasses = await getActionClasses(params.environmentId);
+ return (
+ <>
+
+
+ {actionClasses.map((actionClass) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/events/testURLmatch.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/testURLmatch.ts
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/events/testURLmatch.ts
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/testURLmatch.ts
diff --git a/apps/web/app/(app)/environments/[environmentId]/attributes/AttributeActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/attributes/AttributeActivityTab.tsx
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx
new file mode 100644
index 00000000000..cfce41c9d98
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { Switch } from "@formbricks/ui";
+import { useState } from "react";
+import AttributeDetailModal from "./AttributeDetailModal";
+import UploadAttributesModal from "./UploadAttributesModal";
+import { useMemo } from "react";
+import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
+
+export default function AttributeClassesTable({
+ environmentId,
+ attributeClasses,
+ children: [TableHeading, howToAddAttributeButton, attributeRows],
+}: {
+ environmentId: string;
+ attributeClasses: TAttributeClass[];
+ children: [JSX.Element, JSX.Element, JSX.Element[]];
+}) {
+ const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
+ const [isUploadCSVModalOpen, setUploadCSVModalOpen] = useState(false);
+ const [activeAttributeClass, setActiveAttributeClass] = useState("" as any);
+ const [showArchived, setShowArchived] = useState(false);
+
+ const displayedAttributeClasses = useMemo(() => {
+ return attributeClasses
+ ? showArchived
+ ? attributeClasses
+ : attributeClasses.filter((ac) => !ac.archived)
+ : [];
+ }, [showArchived, attributeClasses]);
+
+ const hasArchived = useMemo(() => {
+ return attributeClasses ? attributeClasses.some((ac) => ac.archived) : false;
+ }, [attributeClasses]);
+
+ const handleOpenAttributeDetailModalClick = (e, attributeClass) => {
+ e.preventDefault();
+ setActiveAttributeClass(attributeClass);
+ setAttributeDetailModalOpen(true);
+ };
+
+ const toggleShowArchived = () => {
+ setShowArchived(!showArchived);
+ };
+
+ return (
+ <>
+
+ {hasArchived && (
+
+ Show archived
+
+
+ )}
+ {howToAddAttributeButton}
+
+
+ {TableHeading}
+
+ {displayedAttributeClasses.map((attributeClass, index) => (
+ {
+ handleOpenAttributeDetailModalClick(e, attributeClass);
+ }}
+ className="w-full"
+ key={attributeClass.id}>
+ {attributeRows[index]}
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/attributes/AttributeDetailModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
similarity index 96%
rename from apps/web/app/(app)/environments/[environmentId]/attributes/AttributeDetailModal.tsx
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
index d78ec43d03e..9501bc2a03e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/attributes/AttributeDetailModal.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeDetailModal.tsx
@@ -27,7 +27,6 @@ export default function AttributeDetailModal({
children: (
),
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData.tsx
new file mode 100644
index 00000000000..ad317ba1566
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData.tsx
@@ -0,0 +1,33 @@
+import { timeSinceConditionally } from "@formbricks/lib/time";
+import { Badge } from "@formbricks/ui";
+import { TagIcon } from "@heroicons/react/24/solid";
+
+export default function AttributeClassDataRow({ attributeClass }) {
+ return (
+
+
+
+
+
+
+
+
+ {attributeClass.name}
+
+ {attributeClass.archived && }
+
+
+
{attributeClass.description}
+
+
+
+
+
+
{timeSinceConditionally(attributeClass.createdAt.toString())}
+
+
+
{timeSinceConditionally(attributeClass.updatedAt.toString())}
+
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/attributes/AttributeSettingsTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeSettingsTab.tsx
similarity index 82%
rename from apps/web/app/(app)/environments/[environmentId]/attributes/AttributeSettingsTab.tsx
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeSettingsTab.tsx
index 6ebdc61af61..55e1d4d86cd 100644
--- a/apps/web/app/(app)/environments/[environmentId]/attributes/AttributeSettingsTab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeSettingsTab.tsx
@@ -1,41 +1,38 @@
-import { useAttributeClasses } from "@/lib/attributeClasses/attributeClasses";
-import { useAttributeClassMutation } from "@/lib/attributeClasses/mutateAttributeClasses";
+"use client";
+
import { Button, Input, Label } from "@formbricks/ui";
import type { AttributeClass } from "@prisma/client";
import { useForm } from "react-hook-form";
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
+import { useRouter } from "next/navigation";
+import { updatetAttributeClass } from "@formbricks/lib/services/attributeClass";
+import { useState } from "react";
interface AttributeSettingsTabProps {
- environmentId: string;
attributeClass: AttributeClass;
setOpen: (v: boolean) => void;
}
-export default function AttributeSettingsTab({
- environmentId,
- attributeClass,
- setOpen,
-}: AttributeSettingsTabProps) {
+export default function AttributeSettingsTab({ attributeClass, setOpen }: AttributeSettingsTabProps) {
+ const router = useRouter();
const { register, handleSubmit } = useForm({
defaultValues: { name: attributeClass.name, description: attributeClass.description },
});
- const { triggerAttributeClassMutate, isMutatingAttributeClass } = useAttributeClassMutation(
- environmentId,
- attributeClass.id
- );
-
- const { mutateAttributeClasses } = useAttributeClasses(environmentId);
+ const [isAttributeBeingSubmitted, setisAttributeBeingSubmitted] = useState(false);
const onSubmit = async (data) => {
- await triggerAttributeClassMutate(data);
- mutateAttributeClasses();
+ setisAttributeBeingSubmitted(true);
setOpen(false);
+ await updatetAttributeClass(attributeClass.id, data);
+ router.refresh();
+ setisAttributeBeingSubmitted(false);
};
const handleArchiveToggle = async () => {
+ setisAttributeBeingSubmitted(true);
const data = { archived: !attributeClass.archived };
- await triggerAttributeClassMutate(data);
- mutateAttributeClasses();
+ await updatetAttributeClass(attributeClass.id, data);
+ setisAttributeBeingSubmitted(false);
};
return (
@@ -101,7 +98,7 @@ export default function AttributeSettingsTab({
{attributeClass.type !== "automatic" && (
-
+
Save changes
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading.tsx
new file mode 100644
index 00000000000..e23dcab899c
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading.tsx
@@ -0,0 +1,11 @@
+export default function AttributeTableHeading() {
+ return (
+ <>
+
+
Name
+
Created
+
Last Updated
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton.tsx
new file mode 100644
index 00000000000..de50ca2715c
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton.tsx
@@ -0,0 +1,14 @@
+import { Button } from "@formbricks/ui";
+import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
+
+export default function HowToAddAttributesButton() {
+ return (
+
+
+ How to add attributes
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/attributes/UploadAttributesModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/attributes/UploadAttributesModal.tsx
rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/layout.tsx
new file mode 100644
index 00000000000..6aad0d78e58
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/layout.tsx
@@ -0,0 +1,11 @@
+import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
+import ContentWrapper from "@/components/shared/ContentWrapper";
+
+export default function ActionsAndAttributesLayout({ params, children }) {
+ return (
+ <>
+
+
{children}
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/loading.tsx
new file mode 100644
index 00000000000..5f2cd2f4ad5
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/loading.tsx
@@ -0,0 +1,55 @@
+import { Button } from "@formbricks/ui";
+import { TagIcon } from "@heroicons/react/24/solid";
+import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
+
+export default function Loading() {
+ return (
+ <>
+
+
+
+
+ Loading Attributes
+
+
+
+
+
+
+
Name
+
Created
+
Last Updated
+
+
+
+ {[...Array(3)].map((_, index) => (
+
+ ))}
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx
new file mode 100644
index 00000000000..e712647352b
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx
@@ -0,0 +1,26 @@
+import AttributeClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable";
+import AttributeClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData";
+import AttributeTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading";
+import HowToAddAttributesButton from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton";
+import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Attributes",
+};
+
+export default async function AttributesPage({ params }) {
+ let attributeClasses = await getAttributeClasses(params.environmentId);
+ return (
+ <>
+
+
+
+
+ {attributeClasses.map((attributeClass) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
index fa15f798a15..aadedc73ce4 100644
--- a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
@@ -117,9 +117,9 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
},
{
name: "Actions & Attributes",
- href: `/environments/${environmentId}/events`,
+ href: `/environments/${environmentId}/actions`,
icon: FilterIcon,
- current: pathname?.includes("/events") || pathname?.includes("/attributes"),
+ current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
},
{
name: "Integrations",
@@ -221,7 +221,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme