diff --git a/apps/web/components/events_attributes/EventsAttributesTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx similarity index 59% rename from apps/web/components/events_attributes/EventsAttributesTabs.tsx rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx index a521d11dd40..7c21c621637 100644 --- a/apps/web/components/events_attributes/EventsAttributesTabs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx @@ -1,18 +1,18 @@ -import SecondNavbar from "../environments/SecondNavBar"; +import SecondNavbar from "@/components/environments/SecondNavBar"; import { CursorArrowRaysIcon, TagIcon } from "@heroicons/react/24/solid"; -interface EventsAttributesTabsProps { +interface ActionsAttributesTabsProps { activeId: string; environmentId: string; } -export default function EventsAttributesTabs({ activeId, environmentId }: EventsAttributesTabsProps) { +export default function ActionsAttributesTabs({ activeId, environmentId }: ActionsAttributesTabsProps) { const tabs = [ { - id: "events", + id: "actions", label: "Actions", icon: , - href: `/environments/${environmentId}/events`, + href: `/environments/${environmentId}/actions`, }, { id: "attributes", diff --git a/apps/web/app/(app)/environments/[environmentId]/events/EventActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx similarity index 95% rename from apps/web/app/(app)/environments/[environmentId]/events/EventActivityTab.tsx rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx index 757dad9b643..c6beed9eaf3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/events/EventActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx @@ -8,11 +8,11 @@ import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/r interface ActivityTabProps { environmentId: string; - eventClassId: string; + actionClassId: string; } -export default function EventActivityTab({ environmentId, eventClassId }: ActivityTabProps) { - const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId); +export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) { + const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId); if (isLoadingEventClass) return ; if (isErrorEventClass) return ; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable.tsx new file mode 100644 index 00000000000..7343a9e5ced --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Button } from "@formbricks/ui"; +import { CursorArrowRaysIcon } from "@heroicons/react/24/solid"; +import { useState } from "react"; +import AddNoCodeActionModal from "./AddNoCodeActionModal"; +import ActionDetailModal from "./ActionDetailModal"; +import { TActionClass } from "@formbricks/types/v1/actionClasses"; + +export default function ActionClassesTable({ + environmentId, + actionClasses, + children: [TableHeading, actionRows], +}: { + environmentId: string; + actionClasses: TActionClass[]; + children: [JSX.Element, JSX.Element[]]; +}) { + const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false); + const [isAddActionModalOpen, setAddActionModalOpen] = useState(false); + + const [activeActionClass, setActiveActionClass] = useState({ + environmentId, + id: "", + name: "", + type: "noCode", + description: "", + noCodeConfig: null, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const handleOpenActionDetailModalClick = (e, actionClass: TActionClass) => { + e.preventDefault(); + setActiveActionClass(actionClass); + setActionDetailModalOpen(true); + }; + + return ( + <> +
+ +
+
+ {TableHeading} +
+ {actionClasses.map((actionClass, index) => ( + + ))} +
+
+ + + + ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/events/EventDetailModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx similarity index 51% rename from apps/web/app/(app)/environments/[environmentId]/events/EventDetailModal.tsx rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx index 114b4395c09..1017545e989 100644 --- a/apps/web/app/(app)/environments/[environmentId]/events/EventDetailModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx @@ -1,31 +1,31 @@ import ModalWithTabs from "@/components/shared/ModalWithTabs"; import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid"; -import type { EventClass } from "@prisma/client"; -import EventActivityTab from "./EventActivityTab"; -import EventSettingsTab from "./EventSettingsTab"; +import EventActivityTab from "./ActionActivityTab"; +import ActionSettingsTab from "./ActionSettingsTab"; +import { TActionClass } from "@formbricks/types/v1/actionClasses"; -interface EventDetailModalProps { +interface ActionDetailModalProps { environmentId: string; open: boolean; setOpen: (v: boolean) => void; - eventClass: EventClass; + actionClass: TActionClass; } -export default function EventDetailModal({ +export default function ActionDetailModal({ environmentId, open, setOpen, - eventClass, -}: EventDetailModalProps) { + actionClass, +}: ActionDetailModalProps) { const tabs = [ { title: "Activity", - children: , + children: , }, { title: "Settings", children: ( - + ), }, ]; @@ -37,16 +37,16 @@ export default function EventDetailModal({ setOpen={setOpen} tabs={tabs} icon={ - eventClass.type === "code" ? ( + actionClass.type === "code" ? ( - ) : eventClass.type === "noCode" ? ( + ) : actionClass.type === "noCode" ? ( - ) : eventClass.type === "automatic" ? ( + ) : actionClass.type === "automatic" ? ( ) : null } - label={eventClass.name} - description={eventClass.description || ""} + label={actionClass.name} + description={actionClass.description || ""} /> ); diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData.tsx new file mode 100644 index 00000000000..9bb2626399b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData.tsx @@ -0,0 +1,31 @@ +import { timeSinceConditionally } from "@formbricks/lib/time"; +import { TActionClass } from "@formbricks/types/v1/actionClasses"; +import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid"; + +export default function ActionClassDataRow({ actionClass }: { actionClass: TActionClass }) { + return ( +
+
+
+
+ {actionClass.type === "code" ? ( + + ) : actionClass.type === "noCode" ? ( + + ) : actionClass.type === "automatic" ? ( + + ) : null} +
+
+
{actionClass.name}
+
{actionClass.description}
+
+
+
+
+ {timeSinceConditionally(actionClass.createdAt.toString())} +
+
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/events/EventSettingsTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx similarity index 86% rename from apps/web/app/(app)/environments/[environmentId]/events/EventSettingsTab.tsx rename to apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx index 0593d270ba5..dcaea3ed48b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/events/EventSettingsTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx @@ -1,11 +1,9 @@ +"use client"; + import DeleteDialog from "@/components/shared/DeleteDialog"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { deleteEventClass, useEventClass, useEventClasses } from "@/lib/eventClasses/eventClasses"; -import { useEventClassMutation } from "@/lib/eventClasses/mutateEventClasses"; -import type { Event, NoCodeConfig } from "@formbricks/types/events"; +import type { NoCodeConfig } from "@formbricks/types/events"; import { Button, - ErrorComponent, Input, Label, RadioGroup, @@ -18,46 +16,46 @@ import { } 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 { toast } from "react-hot-toast"; import { testURLmatch } from "./testURLmatch"; +import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass"; +import { TActionClassInput } from "@formbricks/types/v1/actionClasses"; -interface EventSettingsTabProps { +interface ActionSettingsTabProps { environmentId: string; - eventClassId: string; + actionClass: any; setOpen: (v: boolean) => void; } -export default function EventSettingsTab({ environmentId, eventClassId, setOpen }: EventSettingsTabProps) { - const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId); +export default function ActionSettingsTab({ environmentId, actionClass, setOpen }: ActionSettingsTabProps) { + const router = useRouter(); const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const { register, handleSubmit, control, watch } = useForm({ defaultValues: { - name: eventClass.name, - description: eventClass.description, - noCodeConfig: eventClass.noCodeConfig, + name: actionClass.name, + description: actionClass.description, + noCodeConfig: actionClass.noCodeConfig, }, }); - const { triggerEventClassMutate, isMutatingEventClass } = useEventClassMutation( - environmentId, - eventClass.id - ); - - const { mutateEventClasses } = useEventClasses(environmentId); + const [isUpdatingAction, setIsUpdatingAction] = useState(false); const onSubmit = async (data) => { const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig); - const updatedData: Event = { + const updatedData: TActionClassInput = { ...data, noCodeConfig: filteredNoCodeConfig, type: "noCode", - } as Event; + } as TActionClassInput; - await triggerEventClassMutate(updatedData); - mutateEventClasses(); + setIsUpdatingAction(true); + await updateActionClass(environmentId, actionClass.id, updatedData); + router.refresh(); + setIsUpdatingAction(false); setOpen(false); }; @@ -83,9 +81,6 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen if (match === "no") toast.error("Your survey would not be shown."); }; - if (isLoadingEventClass) return ; - if (isErrorEventClass) return ; - return (
@@ -95,8 +90,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen type="text" placeholder="e.g. Product Team Info" {...register("name", { - value: eventClass.name, - disabled: eventClass.type === "automatic" || eventClass.type === "code" ? true : false, + value: actionClass.name, + disabled: actionClass.type === "automatic" || actionClass.type === "code" ? true : false, })} />
@@ -106,18 +101,18 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen type="text" placeholder="e.g. Triggers when user changed subscription" {...register("description", { - value: eventClass.description, - disabled: eventClass.type === "automatic" ? true : false, + value: actionClass.description, + disabled: actionClass.type === "automatic" ? true : false, })} />
- {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 ( + <> +
+ +
+ +
+
+ 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) => ( + + ))} +
+ + +
+ + ); +} 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" && (
-
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 ( + + ); +} 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 ( + <> +
+
+ +
+
+ +
+
+
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