From 0a1de196aa81c480d2cc47e1726d7d985e57f791 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:48:49 +0530 Subject: [PATCH] chore: moves setup checklist to react server components (#695) * Chore: moves setup checklist to RSC * fix other merge conflictsg * made code refactors * added TAction as return type for getActions * fixed build issues * fix environmentNotice component * refactor EnvironmentNotice component * fix js tests --------- Co-authored-by: Matthias Nannt --- apps/formbricks-com/tsconfig.json | 12 +-- .../settings/api-keys/page.tsx | 27 +++--- .../settings/setup/SetupInstructions.tsx | 14 +-- .../[environmentId]/settings/setup/actions.ts | 8 ++ .../settings/setup/loading.tsx | 50 ++++++++++ .../[environmentId]/settings/setup/page.tsx | 59 ++++++++---- .../[environmentId]/surveys/SurveyList.tsx | 2 +- .../[environmentId]/surveys/page.tsx | 21 ++++- .../components/shared/EnvironmentNotice.tsx | 94 +++++++------------ .../shared/WidgetStatusIndicator.tsx | 52 +++++----- packages/js/tests/__mocks__/apiMock.ts | 2 +- packages/js/tests/index.test.ts | 8 +- packages/lib/services/actions.ts | 45 +++++++++ packages/lib/services/environment.ts | 23 ++++- packages/tsconfig/nextjs.json | 2 +- packages/types/v1/actions.ts | 14 +++ packages/types/v1/environment.ts | 10 +- turbo.json | 4 +- 18 files changed, 291 insertions(+), 156 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/setup/actions.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/setup/loading.tsx create mode 100644 packages/lib/services/actions.ts create mode 100644 packages/types/v1/actions.ts diff --git a/apps/formbricks-com/tsconfig.json b/apps/formbricks-com/tsconfig.json index d9b7448647..3650ddd242 100644 --- a/apps/formbricks-com/tsconfig.json +++ b/apps/formbricks-com/tsconfig.json @@ -1,18 +1,10 @@ { "extends": "@formbricks/tsconfig/nextjs.json", - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "../../packages/types/*.d.ts" - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"], "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "strictNullChecks": true }, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx index 5f8a220b7f..f9c864da0a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx @@ -5,22 +5,27 @@ import SettingsCard from "../SettingsCard"; import SettingsTitle from "../SettingsTitle"; import ApiKeyList from "./ApiKeyList"; import EnvironmentNotice from "@/components/shared/EnvironmentNotice"; +import { getEnvironment } from "@formbricks/lib/services/environment"; export default async function ProfileSettingsPage({ params }) { + const environment = await getEnvironment(params.environmentId); return (
- - - - - - - + + {environment.type === "development" ? ( + + + + ) : ( + + + + )}
); } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/setup/SetupInstructions.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/setup/SetupInstructions.tsx index d10d930b4f..7dc70feb03 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/setup/SetupInstructions.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/setup/SetupInstructions.tsx @@ -4,11 +4,11 @@ import CodeBlock from "@/components/shared/CodeBlock"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { TabBar } from "@formbricks/ui"; import Link from "next/link"; -import Prism from "prismjs"; import "prismjs/themes/prism.css"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { IoLogoHtml5, IoLogoNpm } from "react-icons/io5"; import packageJson from "@/package.json"; +import { WEBAPP_URL } from "@formbricks/lib/constants"; const tabs = [ { id: "npm", label: "NPM", icon: }, @@ -18,10 +18,6 @@ const tabs = [ export default function SetupInstructions({ environmentId }) { const [activeTab, setActiveTab] = useState(tabs[0].id); - useEffect(() => { - Prism.highlightAll(); - }, [activeTab]); - return (
@@ -37,10 +33,8 @@ export default function SetupInstructions({ environmentId }) { if (typeof window !== "undefined") { formbricks.init({ environmentId: "${environmentId}", - apiHost: "${typeof window !== "undefined" && window.location.protocol}//${ - typeof window !== "undefined" && window.location.host - }", - debug: true, // remove when in production + apiHost: "${WEBAPP_URL}", + debug: true, // remove when in production }); }`} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/setup/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/setup/actions.ts new file mode 100644 index 0000000000..977018a935 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/setup/actions.ts @@ -0,0 +1,8 @@ +"use server"; + +import { updateEnvironment } from "@formbricks/lib/services/environment"; +import { TEnvironment, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment"; + +export async function updateEnvironmentAction(environmentId: string, data: Partial): Promise { + return await updateEnvironment(environmentId, data); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/setup/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/setup/loading.tsx new file mode 100644 index 0000000000..4aa143325c --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/setup/loading.tsx @@ -0,0 +1,50 @@ +function LoadingCard({ title, description, skeletonLines }) { + return ( +
+
+

{title}

+

{description}

+
+
+
+ {skeletonLines.map((line, index) => ( +
+
+
+ ))} +
+
+
+ ); +} + +export default function Loading() { + const cards = [ + { + title: "Widget Status", + description: "Check if the Formbricks widget is alive and kicking.", + skeletonLines: [{ classes: "h-32 max-w-full rounded-md" }], + }, + { + title: "How to setup", + description: "Follow these steps to setup the Formbricks widget within your app", + skeletonLines: [ + { classes: "h-6 w-24 rounded-full" }, + { classes: "h-4 w-60 rounded-full" }, + { classes: "h-4 w-60 rounded-full" }, + { classes: "h-6 w-24 rounded-full" }, + { classes: "h-4 w-60 rounded-full" }, + { classes: "h-4 w-60 rounded-full" }, + ], + }, + ]; + + return ( +
+

Setup Checklist

+ {cards.map((card, index) => ( + + ))} +
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/setup/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/setup/page.tsx index 815b6009bb..ee475bae32 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/setup/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/setup/page.tsx @@ -1,24 +1,51 @@ +export const revalidate = REVALIDATION_INTERVAL; + +import { updateEnvironmentAction } from "@/app/(app)/environments/[environmentId]/settings/setup/actions"; +import EnvironmentNotice from "@/components/shared/EnvironmentNotice"; import WidgetStatusIndicator from "@/components/shared/WidgetStatusIndicator"; +import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getActionsByEnvironmentId } from "@formbricks/lib/services/actions"; +import { getEnvironment } from "@formbricks/lib/services/environment"; +import { ErrorComponent } from "@formbricks/ui"; import SettingsCard from "../SettingsCard"; import SettingsTitle from "../SettingsTitle"; -import EnvironmentNotice from "../../../../../../components/shared/EnvironmentNotice"; import SetupInstructions from "./SetupInstructions"; -export default function ProfileSettingsPage({ params }) { - return ( -
- - - - +export default async function ProfileSettingsPage({ params }) { + const [environment, actions] = await Promise.all([ + await getEnvironment(params.environmentId), + getActionsByEnvironmentId(params.environmentId), + ]); - - - - -
+ if (!environment) { + return ; + } + + return ( + <> + {environment && ( +
+ + + + + + + + + +
+ )} + ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx index 21d6054140..4f639f627d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -88,7 +88,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st key={`survey-${survey.id}`} environmentId={environmentId} environment={environment} - otherEnvironment={otherEnvironment} + otherEnvironment={otherEnvironment!} />
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx index baff375dbb..377d4a90e2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx @@ -1,20 +1,35 @@ export const revalidate = REVALIDATION_INTERVAL; +import { updateEnvironmentAction } from "@/app/(app)/environments/[environmentId]/settings/setup/actions"; import ContentWrapper from "@/components/shared/ContentWrapper"; import WidgetStatusIndicator from "@/components/shared/WidgetStatusIndicator"; -import SurveysList from "./SurveyList"; -import { Metadata } from "next"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getActionsByEnvironmentId } from "@formbricks/lib/services/actions"; +import { getEnvironment } from "@formbricks/lib/services/environment"; +import { Metadata } from "next"; +import SurveysList from "./SurveyList"; export const metadata: Metadata = { title: "Your Surveys", }; export default async function SurveysPage({ params }) { + const [environment, actions] = await Promise.all([ + getEnvironment(params.environmentId), + getActionsByEnvironmentId(params.environmentId), + ]); + return ( - + {environment && ( + + )} ); } diff --git a/apps/web/components/shared/EnvironmentNotice.tsx b/apps/web/components/shared/EnvironmentNotice.tsx index 62ba694d67..0212673a65 100644 --- a/apps/web/components/shared/EnvironmentNotice.tsx +++ b/apps/web/components/shared/EnvironmentNotice.tsx @@ -1,69 +1,39 @@ -"use client"; +import { getEnvironments } from "@formbricks/lib/services/environment"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { LightBulbIcon } from "@heroicons/react/24/outline"; +import { headers } from "next/headers"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useEnvironment } from "@/lib/environments/environments"; -import { ErrorComponent } from "@formbricks/ui"; -import { LightBulbIcon } from "@heroicons/react/24/solid"; -import { useRouter } from "next/navigation"; +interface EnvironmentNoticeProps { + environment: TEnvironment; +} -export default function EnvironmentNotice({ - environmentId, - pageType, -}: { - environmentId: string; - pageType: string; -}) { - const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId); - const router = useRouter(); +export default async function EnvironmentNotice({ environment }: EnvironmentNoticeProps) { + const headersList = headers(); + const currentUrl = headersList.get("x-invoke-path") || ""; + const environments = await getEnvironments(environment.productId); + const otherEnvironmentId = environments.find((e) => e.id !== environment.id)?.id || ""; - const changeEnvironment = (environmentType: string) => { - const newEnvironmentId = environment.product.environments.find((e) => e.type === environmentType)?.id; - router.push(`/environments/${newEnvironmentId}/`); + const replaceEnvironmentId = (url: string, newId: string): string => { + const regex = /environments\/([a-zA-Z0-9]+)/; + if (regex.test(url)) { + return url.replace(regex, `environments/${newId}`); + } + return url; }; - if (isLoadingEnvironment) { - return ; - } - - if (isErrorEnvironment) { - return ; - } - if (pageType === "apiSettings") { - return ( -
-
- -

- {environment.type === "production" - ? "You're currently in the production environment, so you can only create production API keys. " - : "You're currently in the development environment, so you can only create development API keys. "} - - changeEnvironment(environment.type === "production" ? "development" : "production") - } - className="ml-1 cursor-pointer underline"> - Switch to {environment.type === "production" ? "Development" : "Production"} now. - -

-
+ return ( +
+
+ +

+ {`You're currently in the ${environment.type} environment.`} + + Switch to {environment.type === "production" ? "Development" : "Production"} now. + +

- ); - } - - if (pageType === "setupChecklist") - return ( -
- {environment.type === "production" && !environment.widgetSetupCompleted && ( - - )} -
- ); +
+ ); } diff --git a/apps/web/components/shared/WidgetStatusIndicator.tsx b/apps/web/components/shared/WidgetStatusIndicator.tsx index 87adc83fa2..ae1390768c 100644 --- a/apps/web/components/shared/WidgetStatusIndicator.tsx +++ b/apps/web/components/shared/WidgetStatusIndicator.tsx @@ -1,32 +1,34 @@ "use client"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useEnvironment } from "@/lib/environments/environments"; -import { useEnvironmentMutation } from "@/lib/environments/mutateEnvironments"; -import { useEvents } from "@/lib/events/events"; import { timeSince } from "@formbricks/lib/time"; -import { Confetti, ErrorComponent } from "@formbricks/ui"; +import { TAction } from "@formbricks/types/v1/actions"; +import { TEnvironment, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment"; +import { Confetti } from "@formbricks/ui"; import { ArrowDownIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; interface WidgetStatusIndicatorProps { - environmentId: string; + environment: TEnvironment; type: "large" | "mini"; + actions: TAction[]; + updateEnvironmentAction: (environmentId: string, data: Partial) => Promise; } -export default function WidgetStatusIndicator({ environmentId, type }: WidgetStatusIndicatorProps) { - const { events, isLoadingEvents, isErrorEvents } = useEvents(environmentId); - const { triggerEnvironmentMutate } = useEnvironmentMutation(environmentId); - const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId); +export default function WidgetStatusIndicator({ + environment, + type, + actions, + updateEnvironmentAction, +}: WidgetStatusIndicatorProps) { const [confetti, setConfetti] = useState(false); useEffect(() => { - if (!environment?.widgetSetupCompleted && events && events.length > 0) { - triggerEnvironmentMutate({ widgetSetupCompleted: true }); + if (!environment?.widgetSetupCompleted && actions && actions.length > 0) { + updateEnvironmentAction(environment.id, { widgetSetupCompleted: true }); } - }, [environment, triggerEnvironmentMutate, events]); + }, [environment, actions]); const stati = { notImplemented: { @@ -45,8 +47,8 @@ export default function WidgetStatusIndicator({ environmentId, type }: WidgetSta }; const status = useMemo(() => { - if (events && events.length > 0) { - const lastEvent = events[0]; + if (actions && actions.length > 0) { + const lastEvent = actions[0]; const currentTime = new Date(); const lastEventTime = new Date(lastEvent.createdAt); const timeDifference = currentTime.getTime() - lastEventTime.getTime(); @@ -60,22 +62,10 @@ export default function WidgetStatusIndicator({ environmentId, type }: WidgetSta } else { return "notImplemented"; } - }, [events]); + }, [actions]); const currentStatus = stati[status]; - if (isLoadingEvents || isLoadingEnvironment) { - return ( -
- -
- ); - } - - if (isErrorEvents || isErrorEnvironment) { - return ; - } - if (type === "large") { return (
{currentStatus.title}

{currentStatus.subtitle}{" "} - {status !== "notImplemented" && {timeSince(events[0].createdAt)}} + {status !== "notImplemented" && {timeSince(actions[0].createdAt.toISOString())}}

{confetti && }
@@ -105,12 +95,12 @@ export default function WidgetStatusIndicator({ environmentId, type }: WidgetSta } if (type === "mini") { return ( - +

{currentStatus.subtitle}{" "} - {status !== "notImplemented" && {timeSince(events[0].createdAt)}} + {status !== "notImplemented" && {timeSince(actions[0].createdAt.toISOString())}}

{ console.log("Checking page url: http://localhost/"); }; -export const mockLogoutResponse = () => { +export const mockResetResponse = () => { fetchMock.mockResponseOnce( JSON.stringify({ data: { diff --git a/packages/js/tests/index.test.ts b/packages/js/tests/index.test.ts index e9d22f7c5f..e94ec4ba16 100644 --- a/packages/js/tests/index.test.ts +++ b/packages/js/tests/index.test.ts @@ -6,7 +6,7 @@ import formbricks from "../src/index"; import { mockEventTrackResponse, mockInitResponse, - mockLogoutResponse, + mockResetResponse, mockRegisterRouteChangeResponse, mockSetCustomAttributeResponse, mockSetEmailIdResponse, @@ -145,9 +145,9 @@ test("Formbricks should register for route change", async () => { expect(consoleLogMock).toHaveBeenCalledWith(expect.stringMatching(/Checking page url/)); }); -test("Formbricks should logout", async () => { - mockLogoutResponse(); - await formbricks.logout(); +test("Formbricks should reset", async () => { + mockResetResponse(); + await formbricks.reset(); const currentStatePerson = formbricks.getPerson(); const currentStatePersonAttributes = currentStatePerson.attributes; diff --git a/packages/lib/services/actions.ts b/packages/lib/services/actions.ts new file mode 100644 index 0000000000..f53fc48abf --- /dev/null +++ b/packages/lib/services/actions.ts @@ -0,0 +1,45 @@ +import { prisma } from "@formbricks/database"; +import { DatabaseError } from "@formbricks/errors"; +import { TAction } from "@formbricks/types/v1/actions"; +import { Prisma } from "@prisma/client"; +import { cache } from "react"; +import "server-only"; + +export const getActionsByEnvironmentId = cache( + async (environmentId: string, limit?: number): Promise => { + try { + const actionsPrisma = await prisma.event.findMany({ + where: { + eventClass: { + environmentId: environmentId, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: limit ? limit : 20, + 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; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + } +); diff --git a/packages/lib/services/environment.ts b/packages/lib/services/environment.ts index 9b2e3c7ab5..fd4b9ad01a 100644 --- a/packages/lib/services/environment.ts +++ b/packages/lib/services/environment.ts @@ -4,10 +4,10 @@ import { z } from "zod"; import { Prisma } from "@prisma/client"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/errors"; import { ZEnvironment } from "@formbricks/types/v1/environment"; -import type { TEnvironment } from "@formbricks/types/v1/environment"; +import type { TEnvironment, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment"; import { cache } from "react"; -export const getEnvironment = cache(async (environmentId: string): Promise => { +export const getEnvironment = cache(async (environmentId: string): Promise => { let environmentPrisma; try { environmentPrisma = await prisma.environment.findUnique({ @@ -75,3 +75,22 @@ export const getEnvironments = cache(async (productId: string): Promise): Promise => { + const newData = { ...data, updatedAt: new Date() }; + let updatedEnvironment; + try { + updatedEnvironment = await prisma.environment.update({ + where: { + id: environmentId, + }, + data: newData, + }); + return updatedEnvironment; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + throw error; + } +}; diff --git a/packages/tsconfig/nextjs.json b/packages/tsconfig/nextjs.json index 362d8d1609..d5010a1bbc 100644 --- a/packages/tsconfig/nextjs.json +++ b/packages/tsconfig/nextjs.json @@ -18,4 +18,4 @@ }, "include": ["src", "next-env.d.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/packages/types/v1/actions.ts b/packages/types/v1/actions.ts new file mode 100644 index 0000000000..0ffb7585f1 --- /dev/null +++ b/packages/types/v1/actions.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; +import { ZActionClass } from "./actionClasses"; + + +export const ZAction = z.object({ + id: z.string(), + createdAt: z.date(), + sessionId: z.string(), + properties: z.record(z.string()), + actionClass: ZActionClass.nullable(), +}); + +export type TAction = z.infer; + diff --git a/packages/types/v1/environment.ts b/packages/types/v1/environment.ts index b12cf452be..8629d6b500 100644 --- a/packages/types/v1/environment.ts +++ b/packages/types/v1/environment.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const ZEnvironment: any = z.object({ +export const ZEnvironment = z.object({ id: z.string().cuid2(), createdAt: z.date(), updatedAt: z.date(), @@ -10,3 +10,11 @@ export const ZEnvironment: any = z.object({ }); export type TEnvironment = z.infer; + +export const ZEnvironmentUpdateInput = z.object({ + type: z.enum(["development", "production"]), + productId: z.string(), + widgetSetupCompleted: z.boolean(), +}); + +export type TEnvironmentUpdateInput = z.infer; diff --git a/turbo.json b/turbo.json index 5763bebcec..505ff65925 100644 --- a/turbo.json +++ b/turbo.json @@ -13,9 +13,7 @@ "@formbricks/demo#go": { "cache": false, "persistent": true, - "dependsOn": [ - "@formbricks/js#build" - ] + "dependsOn": ["@formbricks/js#build"] }, "@formbricks/api#build": { "outputs": ["dist/**"],