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 all 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
13 changes: 12 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"plugins": ["tailwindcss"],
"plugins": ["tailwindcss", "no-restricted-imports"],
"extends": [
"@remix-run/eslint-config",
"@remix-run/eslint-config/node",
Expand Down Expand Up @@ -66,6 +66,17 @@
"caseInsensitive": true
}
}
],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@remix-run/react",
"importNames": ["useSearchParams"]
}
]
}
]
},
"overrides": [
Expand Down
3 changes: 2 additions & 1 deletion app/components/booking/availability-select.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { useSearchParams } from "@remix-run/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

import {
Select,
SelectContent,
Expand Down
3 changes: 2 additions & 1 deletion 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 { useNavigation } from "@remix-run/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import { isFormProcessing } from "~/utils/form";
import {
Select,
Expand Down
4 changes: 3 additions & 1 deletion app/components/bulk-update-dialog/bulk-update-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { forwardRef, useCallback, useEffect } from "react";
import { useFetcher, useLoaderData, useSearchParams } from "@remix-run/react";
import { useFetcher, useLoaderData } from "@remix-run/react";

import { useAtomValue, useSetAtom } from "jotai";
import {
bulkDialogAtom,
Expand All @@ -10,6 +11,7 @@ import {
selectedBulkItemsAtom,
selectedBulkItemsCountAtom,
} from "~/atoms/list";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import type { action } from "~/routes/api+/assets.bulk-update-location";
import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { Category } from "@prisma/client";
import { useSearchParams } from "@remix-run/react";

import { useAtom, useAtomValue } from "jotai";

import { CategorySelectNoCategories } from "~/components/category/category-select-no-categories";

import { Badge } from "~/components/shared/badge";
import { Button } from "~/components/shared/button";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import type { WithDateFields } from "~/modules/types";
import { useCategorySearch } from "../../../category/useCategorySearch";
import Input from "../../../forms/input";
Expand Down
7 changes: 2 additions & 5 deletions app/components/list/filters/search-form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useRef } from "react";
import {
useLoaderData,
useNavigation,
useSearchParams,
} from "@remix-run/react";
import { useLoaderData, useNavigation } from "@remix-run/react";

import Input from "~/components/forms/input";
import { Button } from "~/components/shared/button";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import type { SearchableIndexResponse } from "~/modules/types";
import { isSearching } from "~/utils/form";
import { tw } from "~/utils/tw";
Expand Down
3 changes: 2 additions & 1 deletion app/components/list/filters/sort-by.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
PopoverPortal,
PopoverTrigger,
} from "@radix-ui/react-popover";
import { useNavigation, useSearchParams } from "@remix-run/react";
import { useNavigation } from "@remix-run/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

import { isFormProcessing } from "~/utils/form";
import { tw } from "~/utils/tw";
Expand Down
3 changes: 2 additions & 1 deletion app/components/list/filters/tag/tag-checkbox-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { Category } from "@prisma/client";
import { useSearchParams } from "@remix-run/react";

import { useAtom, useAtomValue } from "jotai";

import { useTagSearch } from "~/components/category/useTagSearch";
import { Badge } from "~/components/shared/badge";
import { Button } from "~/components/shared/button";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import type { WithDateFields } from "~/modules/types";
import Input from "../../../forms/input";
import { CheckIcon, ChevronRight } from "../../../icons/library";
Expand Down
4 changes: 3 additions & 1 deletion app/components/list/pagination/page-number.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useMemo } from "react";
import { NavLink, useSearchParams } from "@remix-run/react";
import { NavLink } from "@remix-run/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

import { getParamsValues } from "~/utils/list";
import { mergeSearchParams } from "~/utils/merge-search-params";
import { tw } from "~/utils/tw";
Expand Down
4 changes: 3 additions & 1 deletion app/components/list/pagination/per-page-items-select.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "~/components/forms/select";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

import type { loader } from "~/routes/_layout+/assets._index";

export default function PerPageItemsSelect() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useCallback } from "react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import { AnimatePresence } from "framer-motion";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

import type { loader } from "~/routes/_layout+/account-details.subscription";
import { Button } from "../shared/button";

Expand Down
3 changes: 2 additions & 1 deletion app/components/welcome/carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from "react";
import { useSearchParams } from "@remix-run/react";

import { ClientOnly } from "remix-utils/client-only";
import { Pagination, Navigation } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import { Button } from "../shared/button";

export default function WelcomeCarousel() {
Expand Down
62 changes: 62 additions & 0 deletions app/hooks/search-params/use-search-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// eslint-disable-next-line no-restricted-imports
import { useSearchParams as remixUseSearchParams } from "@remix-run/react";
// eslint-disable-next-line import/no-cycle
import { useCookieDestroy } from "./utils";

/**
* Get the types from the ReturnType of the original useSearchParams hook
*/
type SearchParamsType = ReturnType<typeof remixUseSearchParams>[0]; // URLSearchParams
type SetSearchParamsType = ReturnType<typeof remixUseSearchParams>[1];

export const useSearchParams = (): [
SearchParamsType,
(
nextInit: Parameters<SetSearchParamsType>[0],
navigateOptions?: Parameters<SetSearchParamsType>[1]
) => void,
] => {
const [searchParams, setSearchParams] = remixUseSearchParams();
const { destroyCookieValues } = useCookieDestroy();

const customSetSearchParams: (
nextInit: Parameters<SetSearchParamsType>[0],
navigateOptions?: Parameters<SetSearchParamsType>[1]
) => void = (nextInit, navigateOptions) => {
const prevParams = new URLSearchParams(searchParams.toString());

const checkAndDestroyCookies = (newParams: URLSearchParams) => {
const removedKeys: string[] = [];
prevParams.forEach((_value, key) => {
if (!newParams.has(key)) {
removedKeys.push(key);
}
});
if (removedKeys.length > 0) {
destroyCookieValues(removedKeys);
}
};

if (typeof nextInit === "function") {
setSearchParams((prev) => {
let newParams = nextInit(prev);
// Ensure newParams is an instance of URLSearchParams
if (!(newParams instanceof URLSearchParams)) {
newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types
}
checkAndDestroyCookies(newParams);
return newParams;
}, navigateOptions);
} else {
let newParams = nextInit;
// Ensure newParams is an instance of URLSearchParams
if (!(newParams instanceof URLSearchParams)) {
newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types
}
checkAndDestroyCookies(newParams);
setSearchParams(newParams, navigateOptions);
}
};

return [searchParams, customSetSearchParams];
};
149 changes: 149 additions & 0 deletions app/hooks/search-params/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useMemo } from "react";
import { useLoaderData, useLocation } from "@remix-run/react";
import Cookies from "js-cookie";

import type { loader } from "~/routes/_layout+/assets._index";
// eslint-disable-next-line import/no-cycle
import { useSearchParams } from "./use-search-params";

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

/**
* Custom hook to gather and return metadata related to the asset index page.
*
* @returns {Object} - An object containing the filters, a boolean indicating if it's the asset index page,
* a URLSearchParams object constructed from the filters, and the organization ID.
*/
export function useAssetIndexMeta() {
const location = useLocation();
const { filters, organizationId } = useLoaderData<typeof loader>();
const isAssetIndexPage = location.pathname === "/assets";
const cookieSearchParams = new URLSearchParams(filters);

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

/**
* Returns a boolean indicating whether any of the specified keys have values
* in the provided cookie search parameters.
*
* @param {string[]} keys - Array of keys (strings) to check in the cookie search parameters.
* @param {URLSearchParams} cookieSearchParams - URLSearchParams object representing the parameters extracted from cookies.
* @returns {boolean} - True if any of the keys exist in the cookie search parameters, otherwise false.
*/
export function checkValueInCookie(
keys: string[],
cookieSearchParams: URLSearchParams
): boolean {
return keys.map((key) => cookieSearchParams.has(key)).some(Boolean);
}

/**
* Custom hook to check if any of the specified keys have values in the URL search parameters or in cookies.
*
* @param {string[]} keys - Array of keys (strings) to check in the URL search parameters and cookies.
* @returns {boolean} - True if any of the keys have values in the search parameters or in the cookies, otherwise false.
*/
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]
);

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

return hasValue || hasValueInCookie;
}

/**
* Function to delete specific keys from the URL search parameters.
*
* @param {string[]} keys - Array of keys (strings) to be deleted from the URL search parameters.
* @param {SetSearchParams} setSearchParams - Function to update the URL search parameters.
*/
export function deleteKeysInSearchParams(
keys: string[],
setSearchParams: SetSearchParams
) {
keys.forEach((key) => {
setSearchParams((prev) => {
prev.delete(key);
return prev;
});
});
}

/**
* Function to delete specific keys from the cookie search parameters and update the cookie.
*
* @param {string} organizationId - The organization ID used to name the cookie.
* @param {string[]} keys - Array of keys (strings) to be deleted from the cookie search parameters.
* @param {URLSearchParams} cookieSearchParams - URLSearchParams object representing the parameters extracted from cookies.
*/
export function destroyCookieValues(
organizationId: string,
keys: string[],
cookieSearchParams: URLSearchParams
) {
keys.forEach((key) => {
cookieSearchParams.delete(key);
});
Cookies.set(`${organizationId}_assetFilter`, cookieSearchParams.toString(), {
path: "/assets",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
expires: 365, // 1 year
});
}

/**
* Custom hook to create a handler for clearing specific keys from URL search parameters and cookies.
*
* @param {string[]} keys - Array of keys (strings) to be cleared from the URL search parameters and cookies.
* @returns {Function} - A function that, when called, clears the specified keys from the URL search parameters and, if on the asset index page, also from the cookies.
*/
export function useClearValueFromParams(...keys: string[]) {
const [, setSearchParams] = useSearchParams();
const { isAssetIndexPage, organizationId, cookieSearchParams } =
useAssetIndexMeta();

function clearValuesFromParams() {
if (isAssetIndexPage) {
destroyCookieValues(organizationId, keys, cookieSearchParams);
deleteKeysInSearchParams(keys, setSearchParams);
return;
}
deleteKeysInSearchParams(keys, setSearchParams);
}

return clearValuesFromParams;
}
/**
* Custom hook to provide a handler for destroying specific keys from cookies if on the asset index page.
*
* @returns {Object} - An object containing the `destroyCookieValues` function that clears specific keys from cookies.
*/
export function useCookieDestroy() {
const { isAssetIndexPage, cookieSearchParams, organizationId } =
useAssetIndexMeta();

/**
* Function to destroy specific keys from cookies if on the asset index page.
*
* @param {string[]} keys - Array of keys (strings) to be removed from the cookies.
*/
function _destroyCookieValues(keys: string[]) {
// Check if the current page is the asset index page
if (isAssetIndexPage) {
// Call the destroyCookieValues utility function to delete keys from cookies and update the cookie
destroyCookieValues(organizationId, keys, cookieSearchParams);
}
}

return { destroyCookieValues: _destroyCookieValues };
}
3 changes: 2 additions & 1 deletion app/hooks/use-model-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { ChangeEvent } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { User } from "@prisma/client";
import type { SerializeFrom } from "@remix-run/node";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import { useSearchParams } from "~/hooks/search-params/use-search-params";
import { type loader, type ModelFilters } from "~/routes/api+/model-filters";
import { transformItemUsingTransformer } from "~/utils/model-filters";
import useFetcherWithReset from "./use-fetcher-with-reset";
Expand Down
3 changes: 2 additions & 1 deletion app/hooks/use-pagination.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from "react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import type { IndexResponse } from "~/components/list";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

/**
* This base hook is used to get all the pagination data and actions
Expand Down
3 changes: 2 additions & 1 deletion app/hooks/use-position.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from "react";
import { useFetcher, useParams, useSearchParams } from "@remix-run/react";
import { useFetcher, useParams } from "@remix-run/react";
import { atom, useAtom } from "jotai";
import { useSearchParams } from "~/hooks/search-params/use-search-params";

const positionAtom = atom<GeolocationCoordinates | null>(null);

Expand Down
Loading
Loading