diff --git a/apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx b/apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx index c99972fbb..06007ff89 100644 --- a/apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx @@ -149,7 +149,12 @@ export const EnvelopesBulkDeleteDialog = ({ - diff --git a/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx b/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx index 84ee783b7..a7e0efa25 100644 --- a/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx +++ b/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx @@ -24,7 +24,7 @@ export const EnvelopesTableBulkActionBar = ({ } return ( -
+
{selectedCount} selected @@ -36,13 +36,7 @@ export const EnvelopesTableBulkActionBar = ({ Move to Folder - diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx index fd271aafe..b0db12bab 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx @@ -7,6 +7,7 @@ import { useParams, useSearchParams } from 'react-router'; import { Link } from 'react-router'; import { z } from 'zod'; +import { useSessionStorage } from '@documenso/lib/client-only/hooks/use-session-storage'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { parseToIntegerArray } from '@documenso/lib/utils/params'; @@ -58,7 +59,10 @@ export default function DocumentsPage() { const [isMovingDocument, setIsMovingDocument] = useState(false); const [documentToMove, setDocumentToMove] = useState(null); - const [rowSelection, setRowSelection] = useState({}); + const [rowSelection, setRowSelection] = useSessionStorage( + 'documents-bulk-selection', + {}, + ); const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); @@ -121,11 +125,6 @@ export default function DocumentsPage() { } }, [data?.stats]); - // Clear selection when navigation or filters change - useEffect(() => { - setRowSelection({}); - }, [folderId, findDocumentSearchParams]); - return (
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx index 955d5194f..d7998a6cc 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx @@ -1,10 +1,11 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { Trans } from '@lingui/react/macro'; import { EnvelopeType } from '@prisma/client'; import { Bird } from 'lucide-react'; import { useParams, useSearchParams } from 'react-router'; +import { useSessionStorage } from '@documenso/lib/client-only/hooks/use-session-storage'; import { FolderType } from '@documenso/lib/types/folder-type'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; @@ -34,7 +35,10 @@ export default function TemplatesPage() { const page = Number(searchParams.get('page')) || 1; const perPage = Number(searchParams.get('perPage')) || 10; - const [rowSelection, setRowSelection] = useState({}); + const [rowSelection, setRowSelection] = useSessionStorage( + 'templates-bulk-selection', + {}, + ); const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); @@ -51,11 +55,6 @@ export default function TemplatesPage() { folderId, }); - // Clear selection when navigation or filters change - useEffect(() => { - setRowSelection({}); - }, [folderId, page, perPage]); - return (
diff --git a/packages/lib/client-only/hooks/use-session-storage.ts b/packages/lib/client-only/hooks/use-session-storage.ts new file mode 100644 index 000000000..e720b2c8b --- /dev/null +++ b/packages/lib/client-only/hooks/use-session-storage.ts @@ -0,0 +1,69 @@ +import * as React from 'react'; +import type { Dispatch, SetStateAction } from 'react'; + +function dispatchStorageEvent(key: string, newValue: string | null) { + window.dispatchEvent(new StorageEvent('storage', { key, newValue })); +} + +const setSessionStorageItem = (key: string, value: T) => { + const stringifiedValue = JSON.stringify(value); + window.sessionStorage.setItem(key, stringifiedValue); + dispatchStorageEvent(key, stringifiedValue); +}; + +const removeSessionStorageItem = (key: string) => { + window.sessionStorage.removeItem(key); + dispatchStorageEvent(key, null); +}; + +const getSessionStorageItem = (key: string) => { + return window.sessionStorage.getItem(key); +}; + +const useSessionStorageSubscribe = (callback: (event: StorageEvent) => void) => { + window.addEventListener('storage', callback); + return () => window.removeEventListener('storage', callback); +}; + +export function useSessionStorage( + key: string, + initialValue: T, +): [T, Dispatch>] { + const serializedInitialValue = JSON.stringify(initialValue); + + const getSnapshot = () => getSessionStorageItem(key); + const getServerSnapshot = () => serializedInitialValue; + + const store = React.useSyncExternalStore( + useSessionStorageSubscribe, + getSnapshot, + getServerSnapshot, + ); + + const setState: Dispatch> = React.useCallback( + (v) => { + try { + const prevValue = store ? JSON.parse(store) : initialValue; + // @ts-expect-error - SetStateAction function check is safe at runtime + const nextState = typeof v === 'function' ? v(prevValue) : v; + + if (nextState === undefined || nextState === null) { + removeSessionStorageItem(key); + } else { + setSessionStorageItem(key, nextState); + } + } catch (e) { + console.warn(e); + } + }, + [key, store, initialValue], + ); + + React.useEffect(() => { + if (getSessionStorageItem(key) === null && typeof initialValue !== 'undefined') { + setSessionStorageItem(key, initialValue); + } + }, [key, initialValue]); + + return [store ? JSON.parse(store) : initialValue, setState]; +}