feat: No responses due to filter state (#2283)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-03-27 19:52:24 +05:30
committed by GitHub
parent 784930d9fc
commit b99b499cde
15 changed files with 190 additions and 50 deletions

View File

@@ -36,6 +36,7 @@ interface ResponsePageProps {
attributes: TSurveyPersonAttributes;
responsesPerPage: number;
membershipRole?: TMembershipRole;
totalResponseCount: number;
}
const ResponsePage = ({
@@ -49,11 +50,13 @@ const ResponsePage = ({
attributes,
responsesPerPage,
membershipRole,
totalResponseCount,
}: ResponsePageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const [responses, setResponses] = useState<TResponse[]>([]);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const [isFetchingFirstPage, setFetchingFirstPage] = useState<boolean>(true);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
@@ -95,17 +98,6 @@ const ResponsePage = ({
}
}, [searchParams, resetState]);
useEffect(() => {
const fetchInitialResponses = async () => {
const responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
};
fetchInitialResponses();
}, [surveyId, filters, responsesPerPage]);
useEffect(() => {
const handleResponsesCount = async () => {
const responseCount = await getResponseCountAction(surveyId, filters);
@@ -114,6 +106,22 @@ const ResponsePage = ({
handleResponsesCount();
}, [filters, surveyId]);
useEffect(() => {
const fetchInitialResponses = async () => {
try {
setFetchingFirstPage(true);
const responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
} finally {
setFetchingFirstPage(false);
}
};
fetchInitialResponses();
}, [surveyId, filters, responsesPerPage]);
useEffect(() => {
setPage(1);
setHasMore(true);
@@ -152,6 +160,9 @@ const ResponsePage = ({
hasMore={hasMore}
deleteResponse={deleteResponse}
updateResponse={updateResponse}
isFetchingFirstPage={isFetchingFirstPage}
responseCount={responseCount}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);

View File

@@ -12,6 +12,7 @@ import { TTag } from "@formbricks/types/tags";
import { TUser } from "@formbricks/types/user";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import SingleResponseCard from "@formbricks/ui/SingleResponseCard";
import { SkeletonLoader } from "@formbricks/ui/SkeletonLoader";
interface ResponseTimelineProps {
environment: TEnvironment;
@@ -24,6 +25,9 @@ interface ResponseTimelineProps {
hasMore: boolean;
updateResponse: (responseId: string, responses: TResponse) => void;
deleteResponse: (responseId: string) => void;
isFetchingFirstPage: boolean;
responseCount: number | null;
totalResponseCount: number;
}
export default function ResponseTimeline({
@@ -36,6 +40,9 @@ export default function ResponseTimeline({
hasMore,
updateResponse,
deleteResponse,
isFetchingFirstPage,
responseCount,
totalResponseCount,
}: ResponseTimelineProps) {
const loadingRef = useRef(null);
@@ -69,11 +76,14 @@ export default function ResponseTimeline({
<div className="space-y-4">
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : responses.length === 0 ? (
) : isFetchingFirstPage ? (
<SkeletonLoader type="response" />
) : responseCount === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
emptyMessage={totalResponseCount === 0 ? undefined : "No response matches your filter"}
/>
) : (
<div>

View File

@@ -6,7 +6,7 @@ import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
@@ -47,6 +47,8 @@ export default async function Page({ params }) {
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
return (
<>
<ResponsePage
@@ -60,6 +62,7 @@ export default async function Page({ params }) {
user={user}
responsesPerPage={RESPONSES_PER_PAGE}
membershipRole={currentUserMembership?.role}
totalResponseCount={totalResponseCount}
/>
</>
);

View File

@@ -9,6 +9,7 @@ import { TSurveySummary } from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { SkeletonLoader } from "@formbricks/ui/SkeletonLoader";
import CTASummary from "./CTASummary";
import DateQuestionSummary from "./DateQuestionSummary";
@@ -23,21 +24,31 @@ interface SummaryListProps {
responseCount: number | null;
environment: TEnvironment;
survey: TSurvey;
fetchingSummary: boolean;
totalResponseCount: number;
}
export default function SummaryList({ summary, environment, responseCount, survey }: SummaryListProps) {
export default function SummaryList({
summary,
environment,
responseCount,
survey,
fetchingSummary,
totalResponseCount,
}: SummaryListProps) {
return (
<div className="mt-10 space-y-8">
{survey.type === "web" && responseCount === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : !responseCount ? (
) : fetchingSummary ? (
<SkeletonLoader type="summary" />
) : responseCount === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
emptyMessage={totalResponseCount === 0 ? undefined : "No response matches your filter"}
/>
) : !summary.length ? (
<EmptySpaceFiller type="summary" environment={environment} />
) : (
summary.map((questionSummary) => {
if (questionSummary.type === TSurveyQuestionType.OpenText) {

View File

@@ -52,6 +52,7 @@ interface SummaryPageProps {
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
membershipRole?: TMembershipRole;
totalResponseCount: number;
}
const SummaryPage = ({
@@ -64,11 +65,13 @@ const SummaryPage = ({
environmentTags,
attributes,
membershipRole,
totalResponseCount,
}: SummaryPageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const [isFetchingSummary, setFetchingSummary] = useState<boolean>(true);
const filters = useMemo(
() => getFormattedFilters(survey, selectedFilter, dateRange),
@@ -77,14 +80,19 @@ const SummaryPage = ({
useEffect(() => {
const handleInitialData = async () => {
const responseCount = await getResponseCountAction(surveyId, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
try {
setFetchingSummary(true);
const responseCount = await getResponseCountAction(surveyId, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
const response = await getSurveySummaryAction(surveyId, filters);
setSurveySummary(response);
} finally {
setFetchingSummary(false);
}
const response = await getSurveySummaryAction(surveyId, filters);
setSurveySummary(response);
};
handleInitialData();
@@ -135,6 +143,8 @@ const SummaryPage = ({
responseCount={responseCount}
survey={survey}
environment={environment}
fetchingSummary={isFetchingSummary}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);

View File

@@ -7,7 +7,7 @@ import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
@@ -55,6 +55,7 @@ export default async function Page({ params }) {
const tags = await getTagsByEnvironmentId(params.environmentId);
const attributes = await getResponsePersonAttributes(params.surveyId);
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
return (
<>
@@ -68,6 +69,7 @@ export default async function Page({ params }) {
environmentTags={tags}
attributes={attributes}
membershipRole={currentUserMembership?.role}
totalResponseCount={totalResponseCount}
/>
</>
);

View File

@@ -53,7 +53,12 @@ const QuestionFilterComboBox = ({
// when question type is multi selection so we remove the option from the options which has been already selected
const options = isMultiple
? filterComboBoxOptions?.filter((o) => !filterComboBoxValue?.includes(o))
? filterComboBoxOptions?.filter(
(o) =>
!filterComboBoxValue?.includes(
typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o
)
)
: filterComboBoxOptions;
// disable the combo box for selection of value when question type is nps or rating and selected value is submitted or skipped

View File

@@ -71,7 +71,9 @@ export const generateQuestionAndFilterOptions = (
questionFilterOptions.push({
type: q.type,
filterOptions: conditionOptions[q.type],
filterComboBoxOptions: q?.choices ? q?.choices?.map((c) => c?.label) : [""],
filterComboBoxOptions: q?.choices
? q?.choices?.filter((c) => c.id !== "other")?.map((c) => c?.label)
: [""],
id: q.id,
});
} else if (q.type === TSurveyQuestionType.PictureSelection) {

View File

@@ -31,6 +31,7 @@ interface ResponsePageProps {
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
responsesPerPage: number;
totalResponseCount: number;
}
const ResponsePage = ({
@@ -42,11 +43,13 @@ const ResponsePage = ({
environmentTags,
attributes,
responsesPerPage,
totalResponseCount,
}: ResponsePageProps) => {
const [responses, setResponses] = useState<TResponse[]>([]);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const [responseCount, setResponseCount] = useState<number | null>(null);
const [isFetchingFirstPage, setFetchingFirstPage] = useState<boolean>(true);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
@@ -82,17 +85,6 @@ const ResponsePage = ({
}
}, [searchParams, resetState]);
useEffect(() => {
const fetchInitialResponses = async () => {
const responses = await getResponsesBySurveySharingKeyAction(sharingKey, 1, responsesPerPage, filters);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
};
fetchInitialResponses();
}, [filters, responsesPerPage, sharingKey]);
useEffect(() => {
const handleResponsesCount = async () => {
const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
@@ -101,6 +93,27 @@ const ResponsePage = ({
handleResponsesCount();
}, [filters, sharingKey]);
useEffect(() => {
const fetchInitialResponses = async () => {
try {
setFetchingFirstPage(true);
const responses = await getResponsesBySurveySharingKeyAction(
sharingKey,
1,
responsesPerPage,
filters
);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
} finally {
setFetchingFirstPage(false);
}
};
fetchInitialResponses();
}, [filters, responsesPerPage, sharingKey]);
return (
<ContentWrapper>
<SummaryHeader survey={survey} product={product} />
@@ -120,6 +133,9 @@ const ResponsePage = ({
environmentTags={environmentTags}
fetchNextPage={fetchNextPage}
hasMore={hasMore}
isFetchingFirstPage={isFetchingFirstPage}
responseCount={responseCount}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);

View File

@@ -11,6 +11,7 @@ import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import SingleResponseCard from "@formbricks/ui/SingleResponseCard";
import { SkeletonLoader } from "@formbricks/ui/SkeletonLoader";
interface ResponseTimelineProps {
environment: TEnvironment;
@@ -20,6 +21,9 @@ interface ResponseTimelineProps {
environmentTags: TTag[];
fetchNextPage: () => void;
hasMore: boolean;
isFetchingFirstPage: boolean;
responseCount: number | null;
totalResponseCount: number;
}
export default function ResponseTimeline({
@@ -29,6 +33,9 @@ export default function ResponseTimeline({
environmentTags,
fetchNextPage,
hasMore,
isFetchingFirstPage,
responseCount,
totalResponseCount,
}: ResponseTimelineProps) {
const loadingRef = useRef(null);
@@ -62,11 +69,14 @@ export default function ResponseTimeline({
<div className="space-y-4">
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : responses.length === 0 ? (
) : isFetchingFirstPage ? (
<SkeletonLoader type="response" />
) : responseCount === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
emptyMessage={totalResponseCount === 0 ? undefined : "No response matches your filter"}
/>
) : (
<div>

View File

@@ -4,7 +4,7 @@ import { notFound } from "next/navigation";
import { RESPONSES_PER_PAGE, REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
@@ -35,6 +35,7 @@ export default async function Page({ params }) {
const tags = await getTagsByEnvironmentId(environment.id);
const attributes = await getResponsePersonAttributes(surveyId);
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
return (
<>
@@ -48,6 +49,7 @@ export default async function Page({ params }) {
environmentTags={tags}
attributes={attributes}
responsesPerPage={RESPONSES_PER_PAGE}
totalResponseCount={totalResponseCount}
/>
</>
);

View File

@@ -46,6 +46,7 @@ interface SummaryPageProps {
sharingKey: string;
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
totalResponseCount: number;
}
const SummaryPage = ({
@@ -56,12 +57,15 @@ const SummaryPage = ({
sharingKey,
environmentTags,
attributes,
totalResponseCount,
}: SummaryPageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const [isFetchingSummary, setFetchingSummary] = useState<boolean>(true);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const filters = useMemo(
() => getFormattedFilters(survey, selectedFilter, dateRange),
@@ -70,14 +74,19 @@ const SummaryPage = ({
useEffect(() => {
const handleInitialData = async () => {
const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
try {
setFetchingSummary(true);
const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
const response = await getSummaryBySurveySharingKeyAction(sharingKey, filters);
setSurveySummary(response);
} finally {
setFetchingSummary(false);
}
const response = await getSummaryBySurveySharingKeyAction(sharingKey, filters);
setSurveySummary(response);
};
handleInitialData();
@@ -119,6 +128,8 @@ const SummaryPage = ({
responseCount={responseCount}
survey={survey}
environment={environment}
fetchingSummary={isFetchingSummary}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);

View File

@@ -4,7 +4,7 @@ import { notFound } from "next/navigation";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
@@ -35,6 +35,7 @@ export default async function Page({ params }) {
const tags = await getTagsByEnvironmentId(environment.id);
const attributes = await getResponsePersonAttributes(surveyId);
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
return (
<>
@@ -46,6 +47,7 @@ export default async function Page({ params }) {
product={product}
environmentTags={tags}
attributes={attributes}
totalResponseCount={totalResponseCount}
/>
</>
);

View File

@@ -67,7 +67,9 @@ const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
</Link>
)}
{(environment.widgetSetupCompleted || noWidgetRequired) && (
<span className="bg-light-background-primary-500 text-center">Waiting for a response 🧘</span>
<span className="bg-light-background-primary-500 text-center">
{emptyMessage ?? "Waiting for a response"} 🧘
</span>
)}
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>

View File

@@ -0,0 +1,43 @@
import { Skeleton } from "../Skeleton";
type SkeletonLoaderProps = {
type: "response" | "summary";
};
export function SkeletonLoader({ type }: SkeletonLoaderProps) {
if (type === "summary") {
return (
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<Skeleton className="group space-y-4 rounded-lg bg-white p-6 ">
<div className="flex items-center space-x-4">
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="flex gap-4">
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
</div>
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</Skeleton>
</div>
);
}
if (type === "response") {
return (
<div className="group space-y-4 rounded-lg bg-white p-6 ">
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></Skeleton>
<Skeleton className=" h-6 w-full rounded-full bg-slate-100"></Skeleton>
</div>
<div className="space-y-4">
<Skeleton className="h-12 w-full rounded-full bg-slate-100"></Skeleton>
<Skeleton className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></Skeleton>
<Skeleton className="h-12 w-full rounded-full bg-slate-50/50"></Skeleton>
</div>
</div>
);
}
}