feat: Add auto-refresh to the analysis view (#3007)

Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-08-15 12:21:26 +05:30
committed by GitHub
parent 49cd06a9b4
commit 4ebe144191
12 changed files with 171 additions and 100 deletions

View File

@@ -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.'
```

View File

@@ -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<number | null>(null);
const [totalResponseCount, setTotalResponseCount] = useState<number | null>(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: <InboxIcon className="h-5 w-5" />,
href: `${url}/responses?referer=true`,
current: pathname?.includes("/responses"),
onClick: () => {
revalidateSurveyIdPath(environmentId, surveyId);
revalidateSurveyIdPath(environmentId, survey.id);
},
},
];

View File

@@ -157,7 +157,7 @@ export const ResponsePage = ({
<>
<div className="flex gap-1.5">
<CustomFilter survey={survey} />
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} />}
</div>
<ResponseTimeline
environment={environment}

View File

@@ -68,9 +68,9 @@ const Page = async ({ params }) => {
}>
<SurveyAnalysisNavigation
environmentId={environment.id}
responseCount={totalResponseCount}
surveyId={survey.id}
survey={survey}
activeId="responses"
initialTotalResponseCount={totalResponseCount}
/>
</PageHeader>
<ResponsePage

View File

@@ -5,20 +5,15 @@ import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import { Confetti } from "@formbricks/ui/Confetti";
import { ShareEmbedSurvey } from "./ShareEmbedSurvey";
interface SummaryMetadataProps {
environment: TEnvironment;
survey: TSurvey;
webAppUrl: string;
user: TUser;
}
export const SuccessMessage = ({ environment, survey, webAppUrl, user }: SummaryMetadataProps) => {
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 (
<>
<ShareEmbedSurvey
survey={survey}
open={showLinkModal}
setOpen={setShowLinkModal}
webAppUrl={webAppUrl}
user={user}
/>
{confetti && <Confetti />}
</>
);
return <>{confetti && <Confetti />}</>;
};

View File

@@ -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<number | null>(null);
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(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 && <SummaryDropOffs dropOff={surveySummary.dropOff} />}
<div className="flex gap-1.5">
<CustomFilter survey={surveyMemoized} />
{!isSharingPage && <ResultsShareButton survey={surveyMemoized} webAppUrl={webAppUrl} user={user} />}
{!isSharingPage && <ResultsShareButton survey={surveyMemoized} webAppUrl={webAppUrl} />}
</div>
<SummaryList
summary={surveySummary.summary}

View File

@@ -4,7 +4,8 @@ import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surve
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { ShareIcon, SquarePenIcon } from "lucide-react";
import { useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
@@ -24,10 +25,36 @@ export const SurveyAnalysisCTA = ({
webAppUrl: string;
user: TUser;
}) => {
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 (
<div className="hidden justify-end gap-x-1.5 sm:flex">
{survey.resultShareKey && (
@@ -41,7 +68,7 @@ export const SurveyAnalysisCTA = ({
variant="secondary"
size="sm"
onClick={() => {
setShowShareSurveyModal(true);
setOpenShareSurveyModal(true);
}}>
<ShareIcon className="h-5 w-5" />
</Button>
@@ -59,13 +86,13 @@ export const SurveyAnalysisCTA = ({
<ShareEmbedSurvey
survey={survey}
open={showShareSurveyModal}
setOpen={setShowShareSurveyModal}
setOpen={setOpenShareSurveyModal}
webAppUrl={webAppUrl}
user={user}
/>
)}
{user && <SuccessMessage environment={environment} survey={survey} webAppUrl={webAppUrl} user={user} />}
{user && <SuccessMessage environment={environment} survey={survey} />}
</div>
);
};

View File

@@ -76,9 +76,9 @@ const Page = async ({ params }) => {
}>
<SurveyAnalysisNavigation
environmentId={environment.id}
responseCount={totalResponseCount}
surveyId={survey.id}
survey={survey}
activeId="summary"
initialTotalResponseCount={totalResponseCount}
/>
</PageHeader>
<SummaryPage

View File

@@ -9,24 +9,20 @@ import { CopyIcon, DownloadIcon, GlobeIcon, LinkIcon } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { ShareEmbedSurvey } from "../(analysis)/summary/components/ShareEmbedSurvey";
import { ShareSurveyResults } from "../(analysis)/summary/components/ShareSurveyResults";
interface ResultsShareButtonProps {
survey: TSurvey;
webAppUrl: string;
user?: TUser;
}
export const ResultsShareButton = ({ survey, webAppUrl, user }: ResultsShareButtonProps) => {
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
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{showLinkModal && user && (
<ShareEmbedSurvey
survey={survey}
open={showLinkModal}
setOpen={setShowLinkModal}
webAppUrl={webAppUrl}
user={user}
/>
)}
{showResultsLinkModal && (
<ShareSurveyResults
open={showResultsLinkModal}

View File

@@ -41,10 +41,10 @@ const Page = async ({ params }) => {
<PageContentWrapper className="w-full">
<PageHeader pageTitle={survey.name}>
<SurveyAnalysisNavigation
surveyId={survey.id}
survey={survey}
environmentId={environment.id}
activeId="responses"
responseCount={totalResponseCount}
initialTotalResponseCount={totalResponseCount}
/>
</PageHeader>
<ResponsePage

View File

@@ -43,10 +43,10 @@ const Page = async ({ params }) => {
<PageContentWrapper className="w-full">
<PageHeader pageTitle={survey.name}>
<SurveyAnalysisNavigation
surveyId={survey.id}
survey={survey}
environmentId={environment.id}
activeId="summary"
responseCount={totalResponseCount}
initialTotalResponseCount={totalResponseCount}
/>
</PageHeader>
<SummaryPage

View File

@@ -25,7 +25,7 @@ test.describe("Survey Create & Submit Response", async () => {
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();