mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
refactor: Migrate activity service (#1471)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
c42d48e242
commit
2e2c22a1db
@@ -1,43 +0,0 @@
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
|
||||
interface ActivityFeedProps {
|
||||
activities: TActivityFeedItem[];
|
||||
sortByDate: boolean;
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
export default function ActivityFeed({ activities, sortByDate, environment }: ActivityFeedProps) {
|
||||
const sortedActivities: TActivityFeedItem[] = activities.sort((a, b) =>
|
||||
sortByDate
|
||||
? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
: new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{sortedActivities.length === 0 ? (
|
||||
<EmptySpaceFiller type={"event"} environment={environment} />
|
||||
) : (
|
||||
<div>
|
||||
{sortedActivities.map((activityItem) => (
|
||||
<li key={activityItem.id} className="list-none">
|
||||
<div className="relative pb-12">
|
||||
<span className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200" aria-hidden="true" />
|
||||
<div className="relative">
|
||||
<ActivityItemPopover activityItem={activityItem}>
|
||||
<div className="flex space-x-3 text-left">
|
||||
<ActivityItemIcon activityItem={activityItem} />
|
||||
<ActivityItemContent activityItem={activityItem} />
|
||||
</div>
|
||||
</ActivityItemPopover>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +1,30 @@
|
||||
import { capitalizeFirstLetter } from "@/app/lib/utils";
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import {
|
||||
CodeBracketIcon,
|
||||
CursorArrowRaysIcon,
|
||||
EyeIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
SparklesIcon,
|
||||
TagIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { TAction } from "@formbricks/types/actions";
|
||||
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export const ActivityItemIcon = ({ activityItem }: { activityItem: TActivityFeedItem }) => (
|
||||
export const ActivityItemIcon = ({ actionItem }: { actionItem: TAction }) => (
|
||||
<div className="h-12 w-12 rounded-full bg-white p-3 text-slate-500 duration-100 ease-in-out group-hover:scale-110 group-hover:text-slate-600">
|
||||
{activityItem.type === "attribute" ? (
|
||||
<TagIcon />
|
||||
) : activityItem.type === "display" ? (
|
||||
<EyeIcon />
|
||||
) : activityItem.type === "event" ? (
|
||||
<div>
|
||||
{activityItem.actionType === "code" && <CodeBracketIcon />}
|
||||
{activityItem.actionType === "noCode" && <CursorArrowRaysIcon />}
|
||||
{activityItem.actionType === "automatic" && <SparklesIcon />}
|
||||
</div>
|
||||
) : (
|
||||
<QuestionMarkCircleIcon />
|
||||
)}
|
||||
<div>
|
||||
{actionItem.actionClass?.type === "code" && <CodeBracketIcon />}
|
||||
{actionItem.actionClass?.type === "noCode" && <CursorArrowRaysIcon />}
|
||||
{actionItem.actionClass?.type === "automatic" && <SparklesIcon />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ActivityItemContent = ({ activityItem }: { activityItem: TActivityFeedItem }) => (
|
||||
export const ActivityItemContent = ({ actionItem }: { actionItem: TAction }) => (
|
||||
<div>
|
||||
<div className="font-semibold text-slate-700">
|
||||
{activityItem.type === "attribute" ? (
|
||||
<p>{capitalizeFirstLetter(activityItem.attributeLabel)} added</p>
|
||||
) : activityItem.type === "display" ? (
|
||||
<p>Seen survey</p>
|
||||
) : activityItem.type === "event" ? (
|
||||
<p>{activityItem.actionLabel} triggered</p>
|
||||
) : (
|
||||
<p>Unknown Activity</p>
|
||||
)}
|
||||
{actionItem.actionClass ? <p>{actionItem.actionClass.name}</p> : <p>Unknown Activity</p>}
|
||||
</div>
|
||||
<div className="text-sm text-slate-400">
|
||||
<time
|
||||
dateTime={formatDistance(activityItem.createdAt, new Date(), {
|
||||
dateTime={formatDistance(actionItem.createdAt, new Date(), {
|
||||
addSuffix: true,
|
||||
})}>
|
||||
{formatDistance(activityItem.createdAt, new Date(), {
|
||||
{formatDistance(actionItem.createdAt, new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</time>
|
||||
@@ -57,10 +33,10 @@ export const ActivityItemContent = ({ activityItem }: { activityItem: TActivityF
|
||||
);
|
||||
|
||||
export const ActivityItemPopover = ({
|
||||
activityItem,
|
||||
actionItem,
|
||||
children,
|
||||
}: {
|
||||
activityItem: TActivityFeedItem;
|
||||
actionItem: TAction;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
@@ -68,39 +44,15 @@ export const ActivityItemPopover = ({
|
||||
<PopoverTrigger className="group">{children}</PopoverTrigger>
|
||||
<PopoverContent className="bg-white">
|
||||
<div>
|
||||
{activityItem.type === "attribute" ? (
|
||||
{actionItem && (
|
||||
<div>
|
||||
<Label className="font-normal text-slate-400">Attribute Label</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{activityItem.attributeLabel}</p>
|
||||
<Label className="font-normal text-slate-400">Attribute Value</Label>
|
||||
<p className="text-sm font-medium text-slate-900">{activityItem.attributeValue}</p>
|
||||
<Label className="font-normal text-slate-400">Action Label</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{actionItem.actionClass!.name}</p>
|
||||
<Label className="font-normal text-slate-400">Action Description</Label>
|
||||
<p className="text-sm font-medium text-slate-900">{actionItem.actionClass!.description}</p>
|
||||
<Label className="font-normal text-slate-400">Action Type</Label>
|
||||
<p className="text-sm font-medium text-slate-900">{actionItem.actionClass!.type}</p>
|
||||
</div>
|
||||
) : activityItem.type === "display" ? (
|
||||
<div>
|
||||
<Label className="font-normal text-slate-400">Survey Name</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{activityItem.displaySurveyName}</p>
|
||||
</div>
|
||||
) : activityItem.type === "event" ? (
|
||||
<div>
|
||||
<div>
|
||||
<Label className="font-normal text-slate-400">Action Display Name</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{activityItem.actionLabel}</p>{" "}
|
||||
<Label className="font-normal text-slate-400">Action Description</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">
|
||||
{activityItem.actionDescription ? (
|
||||
<span>{activityItem.actionDescription}</span>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</p>
|
||||
<Label className="font-normal text-slate-400">Action Type</Label>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{capitalizeFirstLetter(activityItem.actionType)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<QuestionMarkCircleIcon />
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/components/ActivityTimeline";
|
||||
import { getActivityTimeline } from "@formbricks/lib/activity/service";
|
||||
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function ActivitySection({
|
||||
@@ -9,17 +9,18 @@ export default async function ActivitySection({
|
||||
environmentId: string;
|
||||
personId: string;
|
||||
}) {
|
||||
const [activities, environment] = await Promise.all([
|
||||
getActivityTimeline(personId),
|
||||
const [environment, actions] = await Promise.all([
|
||||
getEnvironment(environmentId),
|
||||
getActionsByPersonId(personId, 1),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="md:col-span-1">
|
||||
<ActivityTimeline environment={environment} activities={activities} />
|
||||
<ActivityTimeline environment={environment} actions={actions.slice(0, 10)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import ActivityFeed from "@/app/(app)/environments/[environmentId]/people/[personId]/components/ActivityFeed";
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
import { TAction } from "@formbricks/types/actions";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { ArrowsUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
|
||||
|
||||
export default function ActivityTimeline({
|
||||
environment,
|
||||
activities,
|
||||
actions,
|
||||
}: {
|
||||
environment: TEnvironment;
|
||||
activities: TActivityFeedItem[];
|
||||
actions: TAction[];
|
||||
}) {
|
||||
const [activityAscending, setActivityAscending] = useState(true);
|
||||
const toggleSortActivity = () => {
|
||||
setActivityAscending(!activityAscending);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-6">
|
||||
<h2 className="text-lg font-bold text-slate-700">Activity Timeline</h2>
|
||||
<div className="text-right">
|
||||
<button
|
||||
onClick={toggleSortActivity}
|
||||
className="hover:text-brand-dark flex items-center px-1 text-slate-800">
|
||||
<ArrowsUpDownIcon className="inline h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-700">Actions Timeline</h2>
|
||||
</div>
|
||||
|
||||
<ActivityFeed activities={activities} sortByDate={activityAscending} environment={environment} />
|
||||
<div className="relative">
|
||||
{actions.length === 0 ? (
|
||||
<EmptySpaceFiller type={"event"} environment={environment} />
|
||||
) : (
|
||||
<div>
|
||||
{actions.map(
|
||||
(actionItem, index) =>
|
||||
actionItem && (
|
||||
<li key={actionItem.id} className="list-none">
|
||||
<div className="relative pb-12">
|
||||
{index !== actions.length - 1 && (
|
||||
<span
|
||||
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div className="relative">
|
||||
<ActivityItemPopover actionItem={actionItem}>
|
||||
<div className="flex space-x-3 text-left">
|
||||
<ActivityItemIcon actionItem={actionItem} />
|
||||
<ActivityItemContent actionItem={actionItem} />
|
||||
</div>
|
||||
</ActivityItemPopover>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<div className="relative">
|
||||
{actions.length === 10 && (
|
||||
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,36 +2,44 @@ import {
|
||||
ActivityItemIcon,
|
||||
ActivityItemPopover,
|
||||
} from "@/app/(app)/environments/[environmentId]/people/[personId]/components/ActivityItemComponents";
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
import { TAction } from "@formbricks/types/actions";
|
||||
import { BackIcon } from "@formbricks/ui/icons";
|
||||
import { ArrowsUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
const unifiedList: TActivityFeedItem[] = [
|
||||
const actionItemList: TAction[] = [
|
||||
{
|
||||
id: "clk9o7gnu000kz8kw4nb26o21",
|
||||
type: "event",
|
||||
actionType: "noCode",
|
||||
id: "demoId1",
|
||||
createdAt: new Date(),
|
||||
actionLabel: "Loading User Acitivity",
|
||||
updatedAt: null,
|
||||
attributeLabel: null,
|
||||
attributeValue: null,
|
||||
actionDescription: null,
|
||||
displaySurveyName: null,
|
||||
sessionId: "",
|
||||
properties: {},
|
||||
actionClass: {
|
||||
id: "demoId1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Loading User Acitivity",
|
||||
description: null,
|
||||
type: "automatic",
|
||||
noCodeConfig: null,
|
||||
environmentId: "testEnvironment",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "clk9o7fwc000iz8kw4s0ha0ql",
|
||||
type: "event",
|
||||
actionType: "automatic",
|
||||
id: "demoId2",
|
||||
createdAt: new Date(),
|
||||
actionLabel: "Loading User Session Info",
|
||||
updatedAt: null,
|
||||
attributeLabel: null,
|
||||
attributeValue: null,
|
||||
actionDescription: null,
|
||||
displaySurveyName: null,
|
||||
sessionId: "",
|
||||
properties: {},
|
||||
actionClass: {
|
||||
id: "demoId2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Loading User Acitivity",
|
||||
description: null,
|
||||
type: "automatic",
|
||||
noCodeConfig: null,
|
||||
environmentId: "testEnvironment",
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
@@ -118,17 +126,17 @@ export default function Loading() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{unifiedList.map((activityItem) => (
|
||||
<li key={activityItem.id} className="list-none">
|
||||
{actionItemList.map((actionItem) => (
|
||||
<li key={actionItem.id} className="list-none">
|
||||
<div className="relative pb-12">
|
||||
<span
|
||||
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative animate-pulse cursor-not-allowed select-none">
|
||||
<ActivityItemPopover activityItem={activityItem}>
|
||||
<ActivityItemPopover actionItem={actionItem}>
|
||||
<div className="flex cursor-not-allowed select-none items-center space-x-3">
|
||||
<ActivityItemIcon activityItem={activityItem} />
|
||||
<ActivityItemIcon actionItem={actionItem} />
|
||||
<div className="font-semibold text-slate-700">Loading</div>
|
||||
</div>
|
||||
</ActivityItemPopover>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { revalidateTag } from "next/cache";
|
||||
|
||||
interface RevalidateProps {
|
||||
environmentId?: string;
|
||||
personId?: string;
|
||||
}
|
||||
|
||||
export const actionCache = {
|
||||
@@ -9,6 +10,9 @@ export const actionCache = {
|
||||
byEnvironmentId(environmentId: string): string {
|
||||
return `environments-${environmentId}-actions`;
|
||||
},
|
||||
byPersonId(personId: string): string {
|
||||
return `environments-${personId}-actions`;
|
||||
},
|
||||
},
|
||||
revalidate({ environmentId }: RevalidateProps): void {
|
||||
if (environmentId) {
|
||||
|
||||
@@ -70,6 +70,54 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
|
||||
: action;
|
||||
};
|
||||
|
||||
export const getActionsByPersonId = async (personId: string, page?: number): Promise<TAction[]> => {
|
||||
const actions = await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([personId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
const actionsPrisma = await prisma.event.findMany({
|
||||
where: {
|
||||
session: {
|
||||
personId: personId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
include: {
|
||||
eventClass: true,
|
||||
},
|
||||
});
|
||||
|
||||
const actions: TAction[] = [];
|
||||
// transforming response to type TAction[]
|
||||
actionsPrisma.forEach((action) => {
|
||||
actions.push({
|
||||
id: action.id,
|
||||
createdAt: action.createdAt,
|
||||
sessionId: action.sessionId,
|
||||
properties: action.properties,
|
||||
actionClass: action.eventClass,
|
||||
});
|
||||
});
|
||||
return actions;
|
||||
},
|
||||
[`getActionsByPersonId-${personId}-${page}`],
|
||||
{
|
||||
tags: [actionCache.tag.byPersonId(personId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
// Deserialize dates if caching does not support deserialization
|
||||
return actions.map((action) => ({
|
||||
...action,
|
||||
createdAt: new Date(action.createdAt),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getActionsByEnvironmentId = async (environmentId: string, page?: number): Promise<TAction[]> => {
|
||||
const actions = await unstable_cache(
|
||||
async () => {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { personCache } from "../person/cache";
|
||||
import { formatActivityFeedItemDateFields } from "./util";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
|
||||
export const getActivityTimeline = async (personId: string): Promise<TActivityFeedItem[]> => {
|
||||
const activityFeedItem = await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([personId, ZId]);
|
||||
|
||||
const person = await prisma.person.findUnique({
|
||||
where: {
|
||||
id: personId,
|
||||
},
|
||||
include: {
|
||||
attributes: {
|
||||
include: {
|
||||
attributeClass: true,
|
||||
},
|
||||
},
|
||||
displays: {
|
||||
include: {
|
||||
survey: true,
|
||||
},
|
||||
},
|
||||
sessions: {
|
||||
include: {
|
||||
events: {
|
||||
include: {
|
||||
eventClass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!person) {
|
||||
throw new ResourceNotFoundError("Person", personId);
|
||||
}
|
||||
const { attributes, displays, sessions } = person;
|
||||
|
||||
const unifiedAttributes: TActivityFeedItem[] = attributes.map((attribute) => ({
|
||||
id: attribute.id,
|
||||
type: "attribute",
|
||||
createdAt: attribute.createdAt,
|
||||
updatedAt: attribute.updatedAt,
|
||||
attributeLabel: attribute.attributeClass.name,
|
||||
attributeValue: attribute.value,
|
||||
actionLabel: null,
|
||||
actionDescription: null,
|
||||
actionType: null,
|
||||
displaySurveyName: null,
|
||||
}));
|
||||
const unifiedDisplays: TActivityFeedItem[] = displays.map((display) => ({
|
||||
id: display.id,
|
||||
type: "display",
|
||||
createdAt: display.createdAt,
|
||||
updatedAt: display.updatedAt,
|
||||
attributeLabel: null,
|
||||
attributeValue: null,
|
||||
actionLabel: null,
|
||||
actionDescription: null,
|
||||
actionType: null,
|
||||
displaySurveyName: display.survey.name,
|
||||
}));
|
||||
const unifiedEvents: TActivityFeedItem[] = sessions.flatMap((session) =>
|
||||
session.events.map((event) => ({
|
||||
id: event.id,
|
||||
type: "event",
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: null,
|
||||
attributeLabel: null,
|
||||
attributeValue: null,
|
||||
actionLabel: event.eventClass?.name || null,
|
||||
actionDescription: event.eventClass?.description || null,
|
||||
actionType: event.eventClass?.type || null,
|
||||
displaySurveyName: null,
|
||||
}))
|
||||
);
|
||||
|
||||
const unifiedList: TActivityFeedItem[] = [...unifiedAttributes, ...unifiedDisplays, ...unifiedEvents];
|
||||
|
||||
return unifiedList;
|
||||
},
|
||||
[`getActivityTimeline-${personId}`],
|
||||
{ tags: [personCache.tag.byId(personId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
|
||||
)();
|
||||
|
||||
return formatActivityFeedItemDateFields(activityFeedItem);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TActivityFeedItem } from "@formbricks/types/activity";
|
||||
|
||||
export const formatActivityFeedItemDateFields = (
|
||||
activityFeedItem: TActivityFeedItem[]
|
||||
): TActivityFeedItem[] => {
|
||||
return activityFeedItem.map((item) => ({
|
||||
...item,
|
||||
createdAt: new Date(item.createdAt),
|
||||
updatedAt: item.updatedAt ? new Date(item.updatedAt) : item.updatedAt,
|
||||
}));
|
||||
};
|
||||
Reference in New Issue
Block a user