mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 13:49:54 -06:00
Move Actions & Attributes pages over to server components (#495)
* feat: server rendering of event actions summary page & server actions * chore: renaming event to action and minor refactoring * fix: logging message * delete: unnecessary file * feat: migrate attributes overview page * feat: impl grouped page & layout, logically differentiate attributes and actions * pnpm format * fix: logical addressing of dirs and minot bugs * move: actionsAndAttributes navbar to dedicated dir from components * fix: use server-only build-time checks and move actionsAttributes navbar * revert: unnecessary docker compose changes * resolve merge conflicts dynamically * fix: address feedback comments * use sparkles icon from heroicons * fix updated action not updating in table * remove async from client function due to warning * move router.refresh in AddNoActionModal * small rename * feat: replace swr w server action in ActionSettingsTab * replace custom error with ResourceNotFoundError error class --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
5bfaad9484
commit
3824d95151
@@ -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: <CursorArrowRaysIcon />,
|
||||
href: `/environments/${environmentId}/events`,
|
||||
href: `/environments/${environmentId}/actions`,
|
||||
},
|
||||
{
|
||||
id: "attributes",
|
||||
@@ -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 <LoadingSpinner />;
|
||||
if (isErrorEventClass) return <ErrorComponent />;
|
||||
@@ -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<TActionClass>({
|
||||
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 (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
onClick={() => {
|
||||
setAddActionModalOpen(true);
|
||||
}}>
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
{TableHeading}
|
||||
<div className="grid-cols-7">
|
||||
{actionClasses.map((actionClass, index) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenActionDetailModalClick(e, actionClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={actionClass.id}>
|
||||
{actionRows[index]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ActionDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isActionDetailModalOpen}
|
||||
setOpen={setActionDetailModalOpen}
|
||||
actionClass={activeActionClass}
|
||||
/>
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddActionModalOpen}
|
||||
setOpen={setAddActionModalOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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: <EventActivityTab environmentId={environmentId} eventClassId={eventClass.id} />,
|
||||
children: <EventActivityTab environmentId={environmentId} actionClassId={actionClass.id} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<EventSettingsTab environmentId={environmentId} eventClassId={eventClass.id} setOpen={setOpen} />
|
||||
<ActionSettingsTab environmentId={environmentId} actionClass={actionClass} setOpen={setOpen} />
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -37,16 +37,16 @@ export default function EventDetailModal({
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
icon={
|
||||
eventClass.type === "code" ? (
|
||||
actionClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : eventClass.type === "noCode" ? (
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : eventClass.type === "automatic" ? (
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null
|
||||
}
|
||||
label={eventClass.name}
|
||||
description={eventClass.description || ""}
|
||||
label={actionClass.name}
|
||||
description={actionClass.description || ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -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 (
|
||||
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
|
||||
{actionClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">{actionClass.name}</div>
|
||||
<div className="text-xs text-slate-400">{actionClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
{timeSinceConditionally(actionClass.createdAt.toString())}
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <LoadingSpinner />;
|
||||
if (isErrorEventClass) return <ErrorComponent />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -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,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -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,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<Label>Action Type</Label>
|
||||
{eventClass.type === "code" ? (
|
||||
{actionClass.type === "code" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This is a code action. Please make changes in your code base.
|
||||
</p>
|
||||
) : eventClass.type === "noCode" ? (
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="w-full space-y-4">
|
||||
<Controller
|
||||
@@ -258,7 +253,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : eventClass.type === "automatic" ? (
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This action was created automatically. You cannot make changes to it.
|
||||
</p>
|
||||
@@ -266,7 +261,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-slate-200 py-6">
|
||||
<div>
|
||||
{eventClass.type !== "automatic" && (
|
||||
{actionClass.type !== "automatic" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="warn"
|
||||
@@ -281,9 +276,9 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
Read Docs
|
||||
</Button>
|
||||
</div>
|
||||
{eventClass.type !== "automatic" && (
|
||||
{actionClass.type !== "automatic" && (
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" variant="darkCTA" loading={isMutatingEventClass}>
|
||||
<Button type="submit" variant="darkCTA" loading={isUpdatingAction}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -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.");
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function ActionTableHeading() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<Event>): Promise<void> => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
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.");
|
||||
@@ -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 (
|
||||
<>
|
||||
<ActionsAttributesTabs activeId="actions" environmentId={params.environmentId} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Loading
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-gray-200 text-slate-500"></div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-28 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
|
||||
<ActionTableHeading />
|
||||
{actionClasses.map((actionClass) => (
|
||||
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />
|
||||
))}
|
||||
</ActionClassesTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
{hasArchived && (
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
Show archived
|
||||
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
|
||||
</div>
|
||||
)}
|
||||
{howToAddAttributeButton}
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
{TableHeading}
|
||||
<div className="grid-cols-7">
|
||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenAttributeDetailModalClick(e, attributeClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={attributeClass.id}>
|
||||
{attributeRows[index]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<AttributeDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
/>
|
||||
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,6 @@ export default function AttributeDetailModal({
|
||||
children: (
|
||||
<AttributeSettingsTab
|
||||
attributeClass={attributeClass}
|
||||
environmentId={environmentId}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
@@ -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 (
|
||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
{attributeClass.name}
|
||||
<span className="ml-2">
|
||||
{attributeClass.archived && <Badge text="Archived" type="gray" size="tiny" />}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">{attributeClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{timeSinceConditionally(attributeClass.createdAt.toString())}</div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{timeSinceConditionally(attributeClass.updatedAt.toString())}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
</div>
|
||||
{attributeClass.type !== "automatic" && (
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" variant="darkCTA" loading={isMutatingAttributeClass}>
|
||||
<Button type="submit" variant="darkCTA" loading={isAttributeBeingSubmitted}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function AttributeTableHeading() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function HowToAddAttributesButton() {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="http://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
How to add attributes
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<ActionsAttributesTabs activeId="attributes" environmentId={params.environmentId} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
Loading Attributes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<AttributeClassesTable environmentId={params.environmentId} attributeClasses={attributeClasses}>
|
||||
<AttributeTableHeading />
|
||||
<HowToAddAttributesButton />
|
||||
|
||||
{attributeClasses.map((attributeClass) => (
|
||||
<AttributeClassDataRow key={attributeClass.id} attributeClass={attributeClass} />
|
||||
))}
|
||||
</AttributeClassesTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
|
||||
{environment?.type === "development" && (
|
||||
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
|
||||
You're in development mode. Use it to test surveys, events and attributes.
|
||||
You're in development mode. Use it to test surveys, actions and attributes.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useAttributeClasses } from "@/lib/attributeClasses/attributeClasses";
|
||||
import { Badge, Button, ErrorComponent, Switch } from "@formbricks/ui";
|
||||
import { QuestionMarkCircleIcon, TagIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import AttributeDetailModal from "./AttributeDetailModal";
|
||||
import UploadAttributesModal from "./UploadAttributesModal";
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function AttributeClassesList({ environmentId }: { environmentId: string }) {
|
||||
const { attributeClasses, isLoadingAttributeClasses, isErrorAttributeClasses } =
|
||||
useAttributeClasses(environmentId);
|
||||
|
||||
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]);
|
||||
|
||||
if (isLoadingAttributeClasses) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
if (isErrorAttributeClasses) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const handleOpenAttributeDetailModalClick = (e, attributeClass) => {
|
||||
e.preventDefault();
|
||||
setActiveAttributeClass(attributeClass);
|
||||
setAttributeDetailModalOpen(true);
|
||||
};
|
||||
|
||||
const toggleShowArchived = () => {
|
||||
setShowArchived(!showArchived);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
{hasArchived && (
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
Show archived
|
||||
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="http://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
How to add attributes
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
<div className="grid-cols-7">
|
||||
{displayedAttributeClasses.map((attributeClass) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenAttributeDetailModalClick(e, attributeClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={attributeClass.id}>
|
||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
{attributeClass.name}
|
||||
<span className="ml-2">
|
||||
{attributeClass.archived && <Badge text="Archived" type="gray" size="tiny" />}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">{attributeClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">
|
||||
{timeSinceConditionally(attributeClass.createdAt.toString())}
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">
|
||||
{timeSinceConditionally(attributeClass.updatedAt.toString())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<AttributeDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
/>
|
||||
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import AttributeClassesList from "./AttributeClassesList";
|
||||
import EventsAttributesTabs from "@/components/events_attributes/EventsAttributesTabs";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
|
||||
export default function AttributesPage({ params }) {
|
||||
return (
|
||||
<div className="">
|
||||
<EventsAttributesTabs activeId="attributes" environmentId={params.environmentId} />
|
||||
<ContentWrapper>
|
||||
<AttributeClassesList environmentId={params.environmentId} />
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useEventClasses } from "@/lib/eventClasses/eventClasses";
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
import { Button, ErrorComponent } from "@formbricks/ui";
|
||||
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import AddNoCodeEventModal from "./AddNoCodeEventModal";
|
||||
import EventDetailModal from "./EventDetailModal";
|
||||
|
||||
export default function EventClassesList({ environmentId }) {
|
||||
const { eventClasses, isLoadingEventClasses, isErrorEventClasses, mutateEventClasses } =
|
||||
useEventClasses(environmentId);
|
||||
|
||||
const [isEventDetailModalOpen, setEventDetailModalOpen] = useState(false);
|
||||
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
|
||||
|
||||
const [activeEventClass, setActiveEventClass] = useState("" as any);
|
||||
|
||||
if (isLoadingEventClasses) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorEventClasses) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const handleOpenEventDetailModalClick = (e, eventClass) => {
|
||||
e.preventDefault();
|
||||
setActiveEventClass(eventClass);
|
||||
setEventDetailModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
onClick={() => {
|
||||
setAddEventModalOpen(true);
|
||||
}}>
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="text-center"># Reps</div>
|
||||
<div className="text-center">Created</div>
|
||||
</div>
|
||||
<div className="grid-cols-7">
|
||||
{eventClasses.map((eventClass) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenEventDetailModalClick(e, eventClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={eventClass.id}>
|
||||
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
|
||||
{eventClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : eventClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : eventClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">{eventClass.name}</div>
|
||||
<div className="text-xs text-slate-400">{eventClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
{eventClass._count?.events}
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
{timeSinceConditionally(eventClass.createdAt)}
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<EventDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isEventDetailModalOpen}
|
||||
setOpen={setEventDetailModalOpen}
|
||||
eventClass={activeEventClass}
|
||||
/>
|
||||
<AddNoCodeEventModal
|
||||
environmentId={environmentId}
|
||||
open={isAddEventModalOpen}
|
||||
setOpen={setAddEventModalOpen}
|
||||
mutateEventClasses={mutateEventClasses}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import EventClassesList from "./EventClassesList";
|
||||
import EventsAttributesTabs from "@/components/events_attributes/EventsAttributesTabs";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Actions & Attributes",
|
||||
};
|
||||
|
||||
export default function EventsPage({ params }) {
|
||||
return (
|
||||
<div className="">
|
||||
<EventsAttributesTabs activeId="events" environmentId={params.environmentId} />
|
||||
<ContentWrapper>
|
||||
<EventClassesList environmentId={params.environmentId} />
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import AddNoCodeEventModal from "@/app/(app)/environments/[environmentId]/events/AddNoCodeEventModal";
|
||||
import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useEventClasses } from "@/lib/eventClasses/eventClasses";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -30,8 +30,7 @@ interface WhenToSendCardProps {
|
||||
|
||||
export default function WhenToSendCard({ environmentId, localSurvey, setLocalSurvey }: WhenToSendCardProps) {
|
||||
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
|
||||
const { eventClasses, isLoadingEventClasses, isErrorEventClasses, mutateEventClasses } =
|
||||
useEventClasses(environmentId);
|
||||
const { eventClasses, isLoadingEventClasses, isErrorEventClasses } = useEventClasses(environmentId);
|
||||
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
|
||||
|
||||
const autoClose = localSurvey.autoClose !== null;
|
||||
@@ -257,11 +256,10 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
)}
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
<AddNoCodeEventModal
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddEventModalOpen}
|
||||
setOpen={setAddEventModalOpen}
|
||||
mutateEventClasses={mutateEventClasses}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/errors";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
import "server-only";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
|
||||
import { TActionClass, TActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
const select = {
|
||||
id: true,
|
||||
@@ -34,16 +34,72 @@ export const getActionClasses = async (environmentId: string): Promise<TActionCl
|
||||
}
|
||||
};
|
||||
|
||||
export const createActionClassServerAction = async (environmentId: string, actionClass) => {
|
||||
export const deleteActionClass = async (
|
||||
environmentId: string,
|
||||
actionClassId: string
|
||||
): Promise<TActionClass> => {
|
||||
try {
|
||||
const result = await prisma.eventClass.delete({
|
||||
where: {
|
||||
id: actionClassId,
|
||||
},
|
||||
select,
|
||||
});
|
||||
if (result === null) throw new ResourceNotFoundError("Action", actionClassId);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`Database error when deleting an action with id ${actionClassId} for environment ${environmentId}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const createActionClass = async (
|
||||
environmentId: string,
|
||||
actionClass: TActionClassInput
|
||||
): Promise<TActionClass> => {
|
||||
try {
|
||||
const result = await prisma.eventClass.create({
|
||||
data: {
|
||||
...actionClass,
|
||||
name: actionClass.name,
|
||||
description: actionClass.description,
|
||||
type: actionClass.type,
|
||||
noCodeConfig: actionClass.noCodeConfig
|
||||
? JSON.parse(JSON.stringify(actionClass.noCodeConfig))
|
||||
: undefined,
|
||||
environment: { connect: { id: environmentId } },
|
||||
},
|
||||
select,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when creating an action for environment ${environmentId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateActionClass = async (
|
||||
environmentId: string,
|
||||
actionClassId: string,
|
||||
inputActionClass: Partial<TActionClassInput>
|
||||
): Promise<TActionClass> => {
|
||||
try {
|
||||
const result = await prisma.eventClass.update({
|
||||
where: {
|
||||
id: actionClassId,
|
||||
},
|
||||
data: {
|
||||
name: inputActionClass.name,
|
||||
description: inputActionClass.description,
|
||||
type: inputActionClass.type,
|
||||
noCodeConfig: inputActionClass.noCodeConfig
|
||||
? JSON.parse(JSON.stringify(inputActionClass.noCodeConfig))
|
||||
: undefined,
|
||||
},
|
||||
select,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when updating an action for environment ${environmentId}`);
|
||||
}
|
||||
};
|
||||
|
||||
62
packages/lib/services/attributeClass.ts
Normal file
62
packages/lib/services/attributeClass.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
"use server";
|
||||
import 'server-only'
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/errors";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
export const transformPrismaAttributeClass = (attributeClass): TAttributeClass | null => {
|
||||
if (attributeClass === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transformedAttributeClass: TAttributeClass = {
|
||||
...attributeClass,
|
||||
};
|
||||
|
||||
return transformedAttributeClass;
|
||||
};
|
||||
|
||||
export const getAttributeClasses = async (environmentId: string): Promise<TAttributeClass[]> => {
|
||||
try {
|
||||
let attributeClasses = await prisma.attributeClass.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
const transformedAttributeClasses: TAttributeClass[] = attributeClasses
|
||||
.map(transformPrismaAttributeClass)
|
||||
.filter((attributeClass): attributeClass is TAttributeClass => attributeClass !== null);
|
||||
|
||||
return transformedAttributeClasses;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching attributeClasses for environment ${environmentId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updatetAttributeClass = async (
|
||||
attributeClassId: string,
|
||||
data: { description?: string; archived?: boolean }
|
||||
): Promise<TAttributeClass | null> => {
|
||||
try {
|
||||
let attributeClass = await prisma.attributeClass.update({
|
||||
where: {
|
||||
id: attributeClassId,
|
||||
},
|
||||
data: {
|
||||
description: data.description,
|
||||
archived: data.archived,
|
||||
},
|
||||
});
|
||||
const transformedAttributeClass: TAttributeClass | null = transformPrismaAttributeClass(attributeClass);
|
||||
|
||||
return transformedAttributeClass;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`Database error when updating attribute class with id ${attributeClassId}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -33,3 +33,12 @@ export const ZActionClass = z.object({
|
||||
});
|
||||
|
||||
export type TActionClass = z.infer<typeof ZActionClass>;
|
||||
|
||||
export const ZActionClassInput = z.object({
|
||||
name: z.string(),
|
||||
description: z.union([z.string(), z.null()]),
|
||||
noCodeConfig: z.union([ZActionClassNoCodeConfig, z.null()]),
|
||||
type: z.enum(["code", "noCode", "automatic"]),
|
||||
});
|
||||
|
||||
export type TActionClassInput = z.infer<typeof ZActionClassInput>;
|
||||
|
||||
14
packages/types/v1/attributeClasses.ts
Normal file
14
packages/types/v1/attributeClasses.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZAttributeClass = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
type: z.enum(["code", "noCode", "automatic"]),
|
||||
environmentId: z.string(),
|
||||
archived: z.boolean(),
|
||||
});
|
||||
|
||||
export type TAttributeClass = z.infer<typeof ZAttributeClass>;
|
||||
Reference in New Issue
Block a user