From 4ebe1441919d4e9123bfe866d8d66f61413a4bd0 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:21:26 +0530 Subject: [PATCH] feat: Add auto-refresh to the analysis view (#3007) Co-authored-by: Matti Nannt --- apps/docs/app/self-hosting/one-click/page.mdx | 4 +- .../components/SurveyAnalysisNavigation.tsx | 86 ++++++++++++++++--- .../responses/components/ResponsePage.tsx | 2 +- .../[surveyId]/(analysis)/responses/page.tsx | 4 +- .../summary/components/SuccessMessage.tsx | 29 ++----- .../summary/components/SummaryPage.tsx | 69 ++++++++------- .../summary/components/SurveyAnalysisCTA.tsx | 37 ++++++-- .../[surveyId]/(analysis)/summary/page.tsx | 4 +- .../components/ResultsShareButton.tsx | 23 +---- .../(analysis)/responses/page.tsx | 4 +- .../[sharingKey]/(analysis)/summary/page.tsx | 4 +- apps/web/playwright/survey.spec.ts | 5 +- 12 files changed, 171 insertions(+), 100 deletions(-) diff --git a/apps/docs/app/self-hosting/one-click/page.mdx b/apps/docs/app/self-hosting/one-click/page.mdx index db2c6e1516..a6c71b2b1f 100644 --- a/apps/docs/app/self-hosting/one-click/page.mdx +++ b/apps/docs/app/self-hosting/one-click/page.mdx @@ -296,7 +296,7 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y 🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance! 📁 Created Formbricks Quickstart directory at ./formbricks. 🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)): -docs@formbricks.com +my.hosted.url.com 🔗 Do you want us to set up an HTTPS certificate for you? [Y/n] Y 🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n] @@ -326,7 +326,7 @@ Y 🔗 To edit more variables and deeper config, go to the formbricks/docker-compose.yml, edit the file, and restart the container! 🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance. -🎉 All done! Please setup your Formbricks instance by visiting your domain at https://tls.piyush.formbricks.com. You can check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.' +🎉 All done! Please setup your Formbricks instance by visiting your domain at https://my.hosted.url.com. You can check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.' ``` diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx index 4d74441dc0..5360e00678 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx @@ -1,29 +1,95 @@ "use client"; -import { revalidateSurveyIdPath } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; +import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; +import { + getResponseCountAction, + revalidateSurveyIdPath, +} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; +import { getFormattedFilters } from "@/app/lib/surveys/surveys"; +import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions"; import { InboxIcon, PresentationIcon } from "lucide-react"; -import { useParams, usePathname } from "next/navigation"; +import { useParams, usePathname, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { TSurvey } from "@formbricks/types/surveys/types"; import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation"; interface SurveyAnalysisNavigationProps { environmentId: string; - surveyId: string; - responseCount: number | null; + survey: TSurvey; + initialTotalResponseCount: number | null; activeId: string; } export const SurveyAnalysisNavigation = ({ environmentId, - surveyId, - responseCount, + survey, + initialTotalResponseCount, activeId, }: SurveyAnalysisNavigationProps) => { const pathname = usePathname(); const params = useParams(); + const [filteredResponseCount, setFilteredResponseCount] = useState(null); + const [totalResponseCount, setTotalResponseCount] = useState(initialTotalResponseCount); const sharingKey = params.sharingKey as string; const isSharingPage = !!sharingKey; - const url = isSharingPage ? `/share/${sharingKey}` : `/environments/${environmentId}/surveys/${surveyId}`; + const searchParams = useSearchParams(); + const isShareEmbedModalOpen = searchParams.get("share") === "true"; + + const url = isSharingPage ? `/share/${sharingKey}` : `/environments/${environmentId}/surveys/${survey.id}`; + const { selectedFilter, dateRange } = useResponseFilter(); + + const filters = useMemo( + () => getFormattedFilters(survey, selectedFilter, dateRange), + [selectedFilter, dateRange] + ); + + const latestFiltersRef = useRef(filters); + latestFiltersRef.current = filters; + + const getResponseCount = () => { + if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey); + return getResponseCountAction(survey.id); + }; + + const fetchResponseCount = async () => { + const count = await getResponseCount(); + setTotalResponseCount(count); + }; + + const getFilteredResponseCount = () => { + if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current); + return getResponseCountAction(survey.id, latestFiltersRef.current); + }; + + const fetchFilteredResponseCount = async () => { + const count = await getFilteredResponseCount(); + setFilteredResponseCount(count); + }; + + useEffect(() => { + fetchFilteredResponseCount(); + }, [filters, isSharingPage, sharingKey, survey.id]); + + useEffect(() => { + if (!isShareEmbedModalOpen) { + const interval = setInterval(() => { + fetchResponseCount(); + fetchFilteredResponseCount(); + }, 10000); + + return () => clearInterval(interval); + } + }, [isShareEmbedModalOpen]); + + const getResponseCountString = () => { + if (totalResponseCount === null) return ""; + if (filteredResponseCount === null) return `(${totalResponseCount})`; + + if (totalResponseCount === filteredResponseCount) return `(${totalResponseCount})`; + + return `(${filteredResponseCount} of ${totalResponseCount})`; + }; const navigation = [ { @@ -33,17 +99,17 @@ export const SurveyAnalysisNavigation = ({ href: `${url}/summary?referer=true`, current: pathname?.includes("/summary"), onClick: () => { - revalidateSurveyIdPath(environmentId, surveyId); + revalidateSurveyIdPath(environmentId, survey.id); }, }, { id: "responses", - label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`, + label: `Responses ${getResponseCountString()}`, icon: , href: `${url}/responses?referer=true`, current: pathname?.includes("/responses"), onClick: () => { - revalidateSurveyIdPath(environmentId, surveyId); + revalidateSurveyIdPath(environmentId, survey.id); }, }, ]; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx index 59bde5629e..8d2ae56e48 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx @@ -157,7 +157,7 @@ export const ResponsePage = ({ <>
- {!isSharingPage && } + {!isSharingPage && }
{ }> { +export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) => { const searchParams = useSearchParams(); - const [showLinkModal, setShowLinkModal] = useState(false); const [confetti, setConfetti] = useState(false); const isAppSurvey = survey.type === "app" || survey.type === "website"; @@ -39,26 +34,18 @@ export const SuccessMessage = ({ environment, survey, webAppUrl, user }: Summary position: "bottom-right", } ); - if (survey.type === "link") { - setShowLinkModal(true); - } + // Remove success param from url const url = new URL(window.location.href); url.searchParams.delete("success"); + if (survey.type === "link") { + // Add share param to url to open share embed modal + url.searchParams.set("share", "true"); + } + window.history.replaceState({}, "", url.toString()); } }, [environment, isAppSurvey, searchParams, survey, widgetSetupCompleted]); - return ( - <> - - {confetti && } - - ); + return <>{confetti && }; }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx index 6c0a0b5dda..7de42cce5a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx @@ -14,7 +14,7 @@ import { getSummaryBySurveySharingKeyAction, } from "@/app/share/[sharingKey]/actions"; import { useParams, useSearchParams } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TEnvironment } from "@formbricks/types/environment"; @@ -53,7 +53,6 @@ export const SummaryPage = ({ survey, surveyId, webAppUrl, - user, totalResponseCount, attributeClasses, }: SummaryPageProps) => { @@ -61,6 +60,9 @@ export const SummaryPage = ({ const sharingKey = params.sharingKey as string; const isSharingPage = !!sharingKey; + const searchParams = useSearchParams(); + const isShareEmbedModalOpen = searchParams.get("share") === "true"; + const [responseCount, setResponseCount] = useState(null); const [surveySummary, setSurveySummary] = useState(initialSurveySummary); const [showDropOffs, setShowDropOffs] = useState(false); @@ -69,39 +71,48 @@ export const SummaryPage = ({ const filters = useMemo( () => getFormattedFilters(survey, selectedFilter, dateRange), - - // eslint-disable-next-line react-hooks/exhaustive-deps [selectedFilter, dateRange] ); + // Use a ref to keep the latest state and props + const latestFiltersRef = useRef(filters); + latestFiltersRef.current = filters; + + const getResponseCount = () => { + if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current); + return getResponseCountAction(surveyId, latestFiltersRef.current); + }; + + const getSummary = () => { + if (isSharingPage) return getSummaryBySurveySharingKeyAction(sharingKey, latestFiltersRef.current); + return getSurveySummaryAction(surveyId, latestFiltersRef.current); + }; + + const handleInitialData = async () => { + try { + const updatedResponseCount = await getResponseCount(); + const updatedSurveySummary = await getSummary(); + + setResponseCount(updatedResponseCount); + setSurveySummary(updatedSurveySummary); + } catch (error) { + console.error(error); + } + }; + useEffect(() => { - const handleInitialData = async () => { - try { - let updatedResponseCount; - if (isSharingPage) { - updatedResponseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); - } else { - updatedResponseCount = await getResponseCountAction(surveyId, filters); - } - setResponseCount(updatedResponseCount); - - let updatedSurveySummary; - if (isSharingPage) { - updatedSurveySummary = await getSummaryBySurveySharingKeyAction(sharingKey, filters); - } else { - updatedSurveySummary = await getSurveySummaryAction(surveyId, filters); - } - - setSurveySummary(updatedSurveySummary); - } catch (error) { - console.error(error); - } - }; - handleInitialData(); }, [filters, isSharingPage, sharingKey, surveyId]); - const searchParams = useSearchParams(); + useEffect(() => { + if (!isShareEmbedModalOpen) { + const interval = setInterval(() => { + handleInitialData(); + }, 10000); + + return () => clearInterval(interval); + } + }, [isShareEmbedModalOpen]); const surveyMemoized = useMemo(() => { return replaceHeadlineRecall(survey, "default", attributeClasses); @@ -123,7 +134,7 @@ export const SummaryPage = ({ {showDropOffs && }
- {!isSharingPage && } + {!isSharingPage && }
{ - const [showShareSurveyModal, setShowShareSurveyModal] = useState(false); + const searchParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const [showShareSurveyModal, setShowShareSurveyModal] = useState(searchParams.get("share") === "true"); + const widgetSetupCompleted = survey.type === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted; + useEffect(() => { + if (searchParams.get("share") === "true") { + setShowShareSurveyModal(true); + } else { + setShowShareSurveyModal(false); + } + }, [searchParams]); + + const setOpenShareSurveyModal = (open: boolean) => { + const searchParams = new URLSearchParams(window.location.search); + + if (open) { + searchParams.set("share", "true"); + setShowShareSurveyModal(true); + } else { + searchParams.delete("share"); + setShowShareSurveyModal(false); + } + + router.push(`${pathname}?${searchParams.toString()}`); + }; return (
{survey.resultShareKey && ( @@ -41,7 +68,7 @@ export const SurveyAnalysisCTA = ({ variant="secondary" size="sm" onClick={() => { - setShowShareSurveyModal(true); + setOpenShareSurveyModal(true); }}> @@ -59,13 +86,13 @@ export const SurveyAnalysisCTA = ({ )} - {user && } + {user && }
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index 097f28ffe8..517d5db22b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -76,9 +76,9 @@ const Page = async ({ params }) => { }> { - const [showLinkModal, setShowLinkModal] = useState(false); +export const ResultsShareButton = ({ survey, webAppUrl }: ResultsShareButtonProps) => { const [showResultsLinkModal, setShowResultsLinkModal] = useState(false); const [showPublishModal, setShowPublishModal] = useState(false); @@ -43,7 +39,6 @@ export const ResultsShareButton = ({ survey, webAppUrl, user }: ResultsShareButt .then(() => { toast.success("Results unpublished successfully."); setShowPublishModal(false); - setShowLinkModal(false); }) .catch((error) => { toast.error(`Error: ${error.message}`); @@ -62,12 +57,6 @@ export const ResultsShareButton = ({ survey, webAppUrl, user }: ResultsShareButt fetchSharingKey(); }, [survey.id, webAppUrl]); - useEffect(() => { - if (showResultsLinkModal) { - setShowLinkModal(false); - } - }, [showResultsLinkModal]); - const copyUrlToClipboard = () => { if (typeof window !== "undefined") { const currentUrl = window.location.href; @@ -134,16 +123,6 @@ export const ResultsShareButton = ({ survey, webAppUrl, user }: ResultsShareButt - - {showLinkModal && user && ( - - )} {showResultsLinkModal && ( { { { await page.getByRole("button", { name: "Publish" }).click(); // Get URL - await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary$/); + await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary(\?.*)?$/); await page.getByLabel("Copy survey link to clipboard").click(); url = await page.evaluate("navigator.clipboard.readText()"); }); @@ -435,7 +435,8 @@ test.describe("Multi Language Survey Create", async () => { await page.getByRole("button", { name: "Publish" }).click(); - await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary$/); + // await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary$/); + await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary(\?.*)?$/); await page.getByLabel("Select Language").click(); await page.getByText("German").click(); await page.getByLabel("Copy survey link to clipboard").click();