Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat 1065 filter persistance on asset index #1211

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/components/booking/status-filter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useNavigation, useSearchParams } from "@remix-run/react";
import { useCookieDestory } from "~/hooks/use-search-param-utils";
import { isFormProcessing } from "~/utils/form";
import {
Select,
Expand All @@ -17,11 +18,13 @@ export function StatusFilter({
const disabled = isFormProcessing(navigation.state);
const [searchParams, setSearchParams] = useSearchParams();
const status = searchParams.get("status");
const { destoryCookieValues } = useCookieDestory();

function handleValueChange(value: string) {
setSearchParams((prev) => {
/** If the value is "ALL", we just remove the param */
if (value === "ALL") {
destoryCookieValues(["status"]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit concerned about this approach, because that requires us to never forget to add this when having functions that clear filters and so on.
A suggestion, what if we create our own abstraction of useSearchParams() that handles this and makes sure that every time a value is removed from the search params, we also remove it from the cookie.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not able to figure out a way to do it due to some scenarios.

  1. Not able to differentiate if the user has cleared the filters in the asset page or if the user is directly going to the asset page after some navigations. (in both cases search parameters are empty.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user has search params saved in the cookie, they wont be empty as they come with the server response and useSearchParams checks on the client so I am not sure what you mean here.

prev.delete("status");
return prev;
}
Expand Down
4 changes: 4 additions & 0 deletions app/components/list/filters/search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {

import Input from "~/components/forms/input";
import { Button } from "~/components/shared/button";
import { useCookieDestory } from "~/hooks/use-search-param-utils";
import type { SearchableIndexResponse } from "~/modules/types";
import { isSearching } from "~/utils/form";
import { tw } from "~/utils/tw";
Expand All @@ -18,13 +19,16 @@ export const SearchForm = ({ className }: { className?: string }) => {
useLoaderData<SearchableIndexResponse>();
const { singular } = modelName;

const { destoryCookieValues } = useCookieDestory();

const navigation = useNavigation();
const disabled = isSearching(navigation);
const searchInputRef = useRef<HTMLInputElement>(null);

const label = searchFieldLabel ? searchFieldLabel : `Search by ${singular}`;

function clearSearch() {
destoryCookieValues(["s"]);
setSearchParams((prev) => {
prev.delete("s");

Expand Down
86 changes: 77 additions & 9 deletions app/hooks/use-search-param-utils.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,103 @@
import { useMemo } from "react";
import { useSearchParams } from "@remix-run/react";
import { useLoaderData, useLocation, useSearchParams } from "@remix-run/react";
import Cookies from "js-cookie";
import type { loader } from "~/routes/_layout+/assets._index";

type SetSearchParams = (
setter: (prev: URLSearchParams) => URLSearchParams
) => void;

export function useAssetIndexMeta() {
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved
const location = useLocation();
const { filters, organizationId } = useLoaderData<typeof loader>();
const isAssetIndexPage = location.pathname === "/assets";
const cookieSearchParams = new URLSearchParams(filters);

return { filters, isAssetIndexPage, cookieSearchParams, organizationId };
}

export function checkValueInCookie(
keys: string[],
cookieSearchParams: URLSearchParams
): boolean {
return keys.map((key) => cookieSearchParams.has(key)).some(Boolean);
}

/**
* Returns a Boolean indicating if any values exists for any key passed
*/
export function useSearchParamHasValue(...keys: string[]) {
const [searchParams] = useSearchParams();

const { isAssetIndexPage, cookieSearchParams } = useAssetIndexMeta();
const hasValue = useMemo(
() => keys.map((key) => searchParams.has(key)).some(Boolean),
[keys, searchParams]
);

return hasValue;
const hasValueInCookie =
isAssetIndexPage && checkValueInCookie(keys, cookieSearchParams);

return hasValue || hasValueInCookie;
}

/**
* Returns a handler which can use used to clear all the values
* for specific keys passed as params
*/

export function deleteKeysInSearchParams(
keys: string[],
setSearchParams: SetSearchParams
) {
keys.forEach((key) => {
setSearchParams((prev) => {
prev.delete(key);
return prev;
});
});
}

export function destoryCookieValues(
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved
organizationId: string,
keys: string[],
cookieSearchParams: URLSearchParams
) {
keys.forEach((key) => {
cookieSearchParams.delete(key);
});
Cookies.set(`${organizationId}_assetFilter`, cookieSearchParams.toString(), {
path: "/",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
expires: 365, // 1 year
});
}

export function useClearValueFromParams(...keys: string[]) {
const [, setSearchParams] = useSearchParams();
const { isAssetIndexPage, organizationId, cookieSearchParams } =
useAssetIndexMeta();

function clearValuesFromParams() {
keys.forEach((key) => {
setSearchParams((prev) => {
prev.delete(key);
return prev;
});
});
if (isAssetIndexPage) {
destoryCookieValues(organizationId, keys, cookieSearchParams);
deleteKeysInSearchParams(keys, setSearchParams);
return;
}
deleteKeysInSearchParams(keys, setSearchParams);
}

return clearValuesFromParams;
}

export function useCookieDestory() {
const { isAssetIndexPage, cookieSearchParams, organizationId } =
useAssetIndexMeta();

function _destoryCookieValues(keys: string[]) {
if (isAssetIndexPage) {
destoryCookieValues(organizationId, keys, cookieSearchParams);
}
}
return { destoryCookieValues: _destoryCookieValues };
}
8 changes: 7 additions & 1 deletion app/modules/asset/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@ export async function getPaginatedAndFilterableAssets({
excludeTagsQuery = false,
excludeSearchFromView = false,
excludeLocationQuery = false,
filters = "",
}: {
request: LoaderFunctionArgs["request"];
organizationId: Organization["id"];
Expand All @@ -1376,13 +1377,18 @@ export async function getPaginatedAndFilterableAssets({
excludeCategoriesQuery?: boolean;
excludeTagsQuery?: boolean;
excludeLocationQuery?: boolean;
filters?: string;
/**
* Set to true if you want the query to be performed by directly accessing the assets table
* instead of the AssetSearchView
*/
excludeSearchFromView?: boolean;
}) {
const searchParams = getCurrentSearchParams(request);
const currentFilterParams = new URLSearchParams(filters || "");
const searchParams = filters
? currentFilterParams
: getCurrentSearchParams(request);

const paramsValues = getParamsValues(searchParams);
const status =
searchParams.get("status") === "ALL" // If the value is "ALL", we just remove the param
Expand Down
40 changes: 37 additions & 3 deletions app/routes/_layout+/assets._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
LoaderFunctionArgs,
MetaFunction,
} from "@remix-run/node";
import { json } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import type { ShouldRevalidateFunctionArgs } from "@remix-run/react";
import { useNavigate } from "@remix-run/react";
import { z } from "zod";
Expand Down Expand Up @@ -52,7 +52,7 @@ import { getOrganizationTierLimit } from "~/modules/tier/service.server";
import assetCss from "~/styles/assets.css?url";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { checkExhaustiveSwitch } from "~/utils/check-exhaustive-switch";
import { setCookie, userPrefs } from "~/utils/cookies.server";
import { userPrefs, getFiltersFromRequest } from "~/utils/cookies.server";
import { sendNotification } from "~/utils/emitter/send-notification.server";
import { ShelfError, makeShelfError } from "~/utils/error";
import { data, error, parseData } from "~/utils/http.server";
Expand Down Expand Up @@ -103,6 +103,16 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
});
}),
]);
const {
filters,
serializedCookie: filtersCookie,
redirectNeeded,
} = await getFiltersFromRequest(request, organizationId);

if (filters && redirectNeeded) {
const cookieParams = new URLSearchParams(filters);
return redirect(`/assets?${cookieParams.toString()}`);
}

let [
tierLimit,
Expand Down Expand Up @@ -132,6 +142,7 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
getPaginatedAndFilterableAssets({
request,
organizationId,
filters,
}),
]);

Expand Down Expand Up @@ -159,6 +170,27 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
plural: "assets",
};

const headers = new Headers();
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved

// Append existing cookies (if any)
const cookieHeader = request.headers.get("Cookie");
if (cookieHeader) {
// Extract existing cookies if needed (optional)
// This step may require additional parsing depending on your use case
headers.append("Cookie", cookieHeader);
}

// Append the new Set-Cookie headers
if (filtersCookie) {
headers.append("Set-Cookie", filtersCookie);
}

// Example: Append user preferences cookie (replace with actual logic if needed)
const userPrefsCookie = await userPrefs.serialize(cookie);
if (userPrefsCookie) {
headers.append("Set-Cookie", userPrefsCookie);
}

return json(
data({
header,
Expand All @@ -184,9 +216,11 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
teamMembers,
totalTeamMembers,
rawTeamMembers,
filters,
organizationId,
}),
{
headers: [setCookie(await userPrefs.serialize(cookie))],
headers,
}
);
} catch (cause) {
Expand Down
33 changes: 33 additions & 0 deletions app/utils/cookies.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,36 @@ export async function initializePerPageCookieOnLayout(request: Request) {
}
return cookie;
}

export const createAssetFilterCookie = (orgId: string) =>
createCookie(`${orgId}_assetFilter`, {
path: "/",
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 365, // 1 year
});

export async function getFiltersFromRequest(
request: Request,
organizationId: string
) {
const url = new URL(request.url);
let filters = url.searchParams?.toString();
rajeshj11 marked this conversation as resolved.
Show resolved Hide resolved
const cookieHeader = request.headers.get("Cookie");

const assetFilterCookie = createAssetFilterCookie(organizationId);
if (filters) {
// Override the cookie with query params
// Serialize the new filters into the cookie
const serializedCookie = await assetFilterCookie.serialize(filters);

return { filters, serializedCookie };
} else if (cookieHeader) {
// Use existing cookie filter
filters = (await assetFilterCookie.parse(cookieHeader)) || {};
filters = new URLSearchParams(filters).toString();
return { filters, redirectNeeded: !!filters };
}
return { filters };
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/dom-to-image": "^2.6.7",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.5",
"@types/luxon": "^3.4.2",
"@types/nodemailer": "^6.4.14",
Expand Down
Loading