mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-03 04:51:00 -06:00
feat: Relevance option for sorting (#2968)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b10d398728
commit
4003d21826
@@ -8,6 +8,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { FORMBRICKS_PRODUCT_ID_LS, FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import {
|
||||
TProductConfigChannel,
|
||||
@@ -59,6 +60,14 @@ export const ProductSettings = ({
|
||||
const productionEnvironment = createProductResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(FORMBRICKS_PRODUCT_ID_LS, productionEnvironment.productId);
|
||||
|
||||
// Rmove filters when creating a new product
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
}
|
||||
if (channel !== "link") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else {
|
||||
|
||||
@@ -30,6 +30,7 @@ import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { FORMBRICKS_PRODUCT_ID_LS, FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -114,6 +115,16 @@ export const MainNavigation = ({
|
||||
}
|
||||
}, [organization]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const productId = localStorage.getItem(FORMBRICKS_PRODUCT_ID_LS);
|
||||
const targetProduct = products.find((product) => product.id === productId);
|
||||
if (targetProduct && productId && product && product.id !== targetProduct.id) {
|
||||
router.push(`/products/${targetProduct.id}/`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sortedOrganizations = useMemo(() => {
|
||||
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [organizations]);
|
||||
@@ -140,6 +151,12 @@ export const MainNavigation = ({
|
||||
}, [products]);
|
||||
|
||||
const handleEnvironmentChangeByProduct = (productId: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(FORMBRICKS_PRODUCT_ID_LS, productId);
|
||||
|
||||
// Remove filters when switching products
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
router.push(`/products/${productId}/`);
|
||||
};
|
||||
|
||||
|
||||
@@ -51,5 +51,6 @@ export const updateResponse = async (
|
||||
};
|
||||
|
||||
export const formbricksLogout = async () => {
|
||||
localStorage.clear();
|
||||
return await formbricks.logout();
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ test.describe("JS Package Test", async () => {
|
||||
(await page.waitForSelector("text=Responses")).isVisible();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const impressionsCount = await page.getByRole("button", { name: "Impressions" }).innerText();
|
||||
expect(impressionsCount).toEqual("Impressions\n\n1");
|
||||
|
||||
3
packages/lib/localStorage.ts
Normal file
3
packages/lib/localStorage.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS = "formbricks-surveys-orientation";
|
||||
export const FORMBRICKS_SURVEYS_FILTERS_KEY_LS = "formbricks-surveys-filters";
|
||||
export const FORMBRICKS_PRODUCT_ID_LS = "formbricks-product-id";
|
||||
@@ -277,6 +277,74 @@ export const getSurveysByActionClassId = reactCache(
|
||||
)()
|
||||
);
|
||||
|
||||
export const getSurveysSortedByRelevance = reactCache(
|
||||
(
|
||||
environmentId: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
filterCriteria?: TSurveyFilterCriteria
|
||||
): Promise<TSurvey[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
let surveys: TSurvey[] = [];
|
||||
const inProgressSurveyCount = await getInProgressSurveyCount(environmentId, filterCriteria);
|
||||
|
||||
// Fetch surveys that are in progress first
|
||||
const inProgressSurveys =
|
||||
offset && offset > inProgressSurveyCount
|
||||
? []
|
||||
: await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
status: "inProgress",
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: buildOrderByClause("updatedAt"),
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
surveys = inProgressSurveys.map(transformPrismaSurvey);
|
||||
|
||||
// Determine if additional surveys are needed
|
||||
if (offset !== undefined && limit && inProgressSurveys.length < limit) {
|
||||
const remainingLimit = limit - inProgressSurveys.length;
|
||||
const newOffset = Math.max(0, offset - inProgressSurveyCount);
|
||||
const additionalSurveys = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
status: { not: "inProgress" },
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: buildOrderByClause("updatedAt"),
|
||||
take: remainingLimit,
|
||||
skip: newOffset,
|
||||
});
|
||||
|
||||
surveys = [...surveys, ...additionalSurveys.map(transformPrismaSurvey)];
|
||||
}
|
||||
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
|
||||
{
|
||||
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
export const getSurveys = reactCache(
|
||||
(
|
||||
environmentId: string,
|
||||
@@ -287,35 +355,33 @@ export const getSurveys = reactCache(
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
|
||||
let surveysPrisma;
|
||||
|
||||
try {
|
||||
surveysPrisma = await prisma.survey.findMany({
|
||||
if (filterCriteria?.sortBy === "relevance") {
|
||||
// Call the sortByRelevance function
|
||||
return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
|
||||
}
|
||||
|
||||
// Fetch surveys normally with pagination
|
||||
const surveysPrisma = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: buildOrderByClause(filterCriteria?.sortBy),
|
||||
take: limit ? limit : undefined,
|
||||
skip: offset ? offset : undefined,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
return surveysPrisma.map(transformPrismaSurvey);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const surveys: TSurvey[] = [];
|
||||
|
||||
for (const surveyPrisma of surveysPrisma) {
|
||||
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
|
||||
surveys.push(transformedSurvey);
|
||||
}
|
||||
return surveys;
|
||||
},
|
||||
[`getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
|
||||
{
|
||||
@@ -353,6 +419,37 @@ export const getSurveyCount = reactCache(
|
||||
)()
|
||||
);
|
||||
|
||||
export const getInProgressSurveyCount = reactCache(
|
||||
(environmentId: string, filterCriteria?: TSurveyFilterCriteria): Promise<number> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
const surveyCount = await prisma.survey.count({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
...buildWhereClause(filterCriteria),
|
||||
},
|
||||
});
|
||||
|
||||
return surveyCount;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getInProgressSurveyCount-${environmentId}-${JSON.stringify(filterCriteria)}`],
|
||||
{
|
||||
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
|
||||
validateInputs([updatedSurvey, ZSurvey]);
|
||||
try {
|
||||
|
||||
@@ -70,15 +70,13 @@ export const buildWhereClause = (filterCriteria?: TSurveyFilterCriteria) => {
|
||||
export const buildOrderByClause = (
|
||||
sortBy?: TSurveyFilterCriteria["sortBy"]
|
||||
): Prisma.SurveyOrderByWithRelationInput[] | undefined => {
|
||||
if (!sortBy) {
|
||||
return undefined;
|
||||
}
|
||||
const orderMapping: { [key: string]: Prisma.SurveyOrderByWithRelationInput } = {
|
||||
name: { name: "asc" },
|
||||
createdAt: { createdAt: "desc" },
|
||||
updatedAt: { updatedAt: "desc" },
|
||||
};
|
||||
|
||||
if (sortBy === "name") {
|
||||
return [{ name: "asc" }];
|
||||
}
|
||||
|
||||
return [{ [sortBy]: "desc" }];
|
||||
return sortBy ? [orderMapping[sortBy] || { updatedAt: "desc" }] : undefined;
|
||||
};
|
||||
|
||||
export const anySurveyHasFilters = (surveys: TSurvey[]): boolean => {
|
||||
|
||||
@@ -1460,7 +1460,7 @@ export const ZSurveyFilterCriteria = z.object({
|
||||
value: z.array(z.enum(["you", "others"])),
|
||||
})
|
||||
.optional(),
|
||||
sortBy: z.enum(["createdAt", "updatedAt", "name"]).optional(),
|
||||
sortBy: z.enum(["createdAt", "updatedAt", "name", "relevance"]).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyFilterCriteria = z.infer<typeof ZSurveyFilterCriteria>;
|
||||
@@ -1470,7 +1470,7 @@ const ZSurveyFilters = z.object({
|
||||
createdBy: z.array(z.enum(["you", "others"])),
|
||||
status: z.array(ZSurveyStatus),
|
||||
type: z.array(ZSurveyType),
|
||||
sortBy: z.enum(["createdAt", "updatedAt", "name"]),
|
||||
sortBy: z.enum(["createdAt", "updatedAt", "name", "relevance"]),
|
||||
});
|
||||
|
||||
export type TSurveyFilters = z.infer<typeof ZSurveyFilters>;
|
||||
@@ -1484,7 +1484,7 @@ export type TFilterOption = z.infer<typeof ZFilterOption>;
|
||||
|
||||
const ZSortOption = z.object({
|
||||
label: z.string(),
|
||||
value: z.enum(["createdAt", "updatedAt", "name"]),
|
||||
value: z.enum(["createdAt", "updatedAt", "name", "relevance"]),
|
||||
});
|
||||
|
||||
export type TSortOption = z.infer<typeof ZSortOption>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChevronDownIcon, Equal, Grid2X2, Search, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import { FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { initialFilters } from "..";
|
||||
@@ -44,7 +45,10 @@ const sortOptions: TSortOption[] = [
|
||||
label: "Alphabetical",
|
||||
value: "name",
|
||||
},
|
||||
// Add other sorting options as needed
|
||||
{
|
||||
label: "Relevance",
|
||||
value: "relevance",
|
||||
},
|
||||
];
|
||||
|
||||
const getToolTipContent = (orientation: string) => {
|
||||
@@ -125,7 +129,7 @@ export const SurveyFilters = ({
|
||||
|
||||
const handleOrientationChange = (value: string) => {
|
||||
setOrientation(value);
|
||||
localStorage.setItem("surveyOrientation", value);
|
||||
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -183,6 +187,7 @@ export const SurveyFilters = ({
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSurveyFilters(initialFilters);
|
||||
localStorage.removeItem("surveyFilters");
|
||||
}}
|
||||
className="h-8"
|
||||
EndIcon={X}
|
||||
@@ -197,7 +202,9 @@ export const SurveyFilters = ({
|
||||
tooltipContent={getToolTipContent("List")}
|
||||
className="bg-slate-900 text-white">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${orientation === "list" ? "bg-slate-900 text-white" : "bg-white"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
|
||||
orientation === "list" ? "bg-slate-900 text-white" : "bg-white"
|
||||
}`}
|
||||
onClick={() => handleOrientationChange("list")}>
|
||||
<Equal className="h-5 w-5" />
|
||||
</div>
|
||||
@@ -208,7 +215,9 @@ export const SurveyFilters = ({
|
||||
tooltipContent={getToolTipContent("Grid")}
|
||||
className="bg-slate-900 text-white">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${orientation === "grid" ? "bg-slate-900 text-white" : "bg-white"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
|
||||
orientation === "grid" ? "bg-slate-900 text-white" : "bg-white"
|
||||
}`}
|
||||
onClick={() => handleOrientationChange("grid")}>
|
||||
<Grid2X2 className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FORMBRICKS_SURVEYS_FILTERS_KEY_LS,
|
||||
FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS,
|
||||
} from "@formbricks/lib/localStorage";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { wrapThrows } from "@formbricks/types/error-handlers";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyFilters } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "../Button";
|
||||
@@ -26,7 +31,7 @@ export const initialFilters: TSurveyFilters = {
|
||||
createdBy: [],
|
||||
status: [],
|
||||
type: [],
|
||||
sortBy: "updatedAt",
|
||||
sortBy: "relevance",
|
||||
};
|
||||
|
||||
export const SurveysList = ({
|
||||
@@ -43,42 +48,65 @@ export const SurveysList = ({
|
||||
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||
|
||||
const [surveyFilters, setSurveyFilters] = useState<TSurveyFilters>(initialFilters);
|
||||
const [isFilterInitialized, setIsFilterInitialized] = useState(false);
|
||||
|
||||
const filters = useMemo(() => getFormattedFilters(surveyFilters, userId), [surveyFilters, userId]);
|
||||
|
||||
const [orientation, setOrientation] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize orientation state with a function that checks if window is defined
|
||||
const orientationFromLocalStorage = localStorage.getItem("surveyOrientation");
|
||||
if (orientationFromLocalStorage) {
|
||||
setOrientation(orientationFromLocalStorage);
|
||||
} else {
|
||||
setOrientation("grid");
|
||||
localStorage.setItem("surveyOrientation", "grid");
|
||||
if (typeof window !== "undefined") {
|
||||
const orientationFromLocalStorage = localStorage.getItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS);
|
||||
if (orientationFromLocalStorage) {
|
||||
setOrientation(orientationFromLocalStorage);
|
||||
} else {
|
||||
setOrientation("grid");
|
||||
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, "grid");
|
||||
}
|
||||
|
||||
const savedFilters = localStorage.getItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
if (savedFilters) {
|
||||
const surveyParseResult = wrapThrows(() => JSON.parse(savedFilters))();
|
||||
|
||||
if (!surveyParseResult.ok) {
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
setSurveyFilters(initialFilters);
|
||||
} else {
|
||||
setSurveyFilters(surveyParseResult.data);
|
||||
}
|
||||
}
|
||||
setIsFilterInitialized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialSurveys = async () => {
|
||||
setIsFetching(true);
|
||||
const res = await getSurveysAction({
|
||||
environmentId: environment.id,
|
||||
limit: surveysLimit,
|
||||
offset: undefined,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
if (res?.data) {
|
||||
if (res.data.length < surveysLimit) {
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setHasMore(true);
|
||||
if (isFilterInitialized) {
|
||||
localStorage.setItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS, JSON.stringify(surveyFilters));
|
||||
}
|
||||
}, [surveyFilters, isFilterInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFilterInitialized) {
|
||||
const fetchInitialSurveys = async () => {
|
||||
setIsFetching(true);
|
||||
const res = await getSurveysAction({
|
||||
environmentId: environment.id,
|
||||
limit: surveysLimit,
|
||||
offset: undefined,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
if (res?.data) {
|
||||
if (res.data.length < surveysLimit) {
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setHasMore(true);
|
||||
}
|
||||
setSurveys(res.data);
|
||||
setIsFetching(false);
|
||||
}
|
||||
setSurveys(res.data);
|
||||
setIsFetching(false);
|
||||
}
|
||||
};
|
||||
fetchInitialSurveys();
|
||||
};
|
||||
fetchInitialSurveys();
|
||||
}
|
||||
}, [environment.id, surveysLimit, filters]);
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user