feat(web): persist list-page filters in URL search params

Resources, projects, and allocations filter state now syncs to/from
URL so filters survive refresh and can be shared via link.
Text inputs are debounced (300ms) to avoid URL churn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 17:00:30 +02:00
parent 7264f0728a
commit 6bf60c8e07
4 changed files with 172 additions and 46 deletions
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { useUrlFilters } from "~/hooks/useUrlFilters.js";
import { useLocalStorage } from "~/hooks/useLocalStorage.js";
import { formatDate } from "~/lib/format.js";
import { trpc } from "~/lib/trpc/client.js";
@@ -106,12 +107,29 @@ function getAllocationQueryFailure(error: AllocationQueryError) {
export function AllocationsClient() {
const [modalOpen, setModalOpen] = useState(false);
const [editingAllocation, setEditingAllocation] = useState<AllocationWithDetails | null>(null);
const [filterProjectId, setFilterProjectId] = useState<string>("");
const [filterResourceId, setFilterResourceId] = useState<string>("");
const [filterStatus, setFilterStatus] = useState<string>("");
const [hidePastProjects, setHidePastProjects] = useState(true);
const [hideCompletedProjects, setHideCompletedProjects] = useState(true);
const [hideDraftProjects, setHideDraftProjects] = useState(false);
const [allocFilters, setAllocFilters] = useUrlFilters({
projectId: "",
resourceId: "",
status: "",
hidePast: "true",
hideCompleted: "true",
hideDraft: "false",
});
const filterProjectId = allocFilters.projectId;
const filterResourceId = allocFilters.resourceId;
const filterStatus = allocFilters.status;
const hidePastProjects = allocFilters.hidePast === "true";
const hideCompletedProjects = allocFilters.hideCompleted === "true";
const hideDraftProjects = allocFilters.hideDraft === "true";
const setFilterProjectId = useCallback((v: string) => setAllocFilters({ projectId: v }), [setAllocFilters]);
const setFilterResourceId = useCallback((v: string) => setAllocFilters({ resourceId: v }), [setAllocFilters]);
const setFilterStatus = useCallback((v: string) => setAllocFilters({ status: v }), [setAllocFilters]);
const setHidePastProjects = useCallback((v: boolean) => setAllocFilters({ hidePast: v ? "true" : "false" }), [setAllocFilters]);
const setHideCompletedProjects = useCallback((v: boolean) => setAllocFilters({ hideCompleted: v ? "true" : "false" }), [setAllocFilters]);
const setHideDraftProjects = useCallback((v: boolean) => setAllocFilters({ hideDraft: v ? "true" : "false" }), [setAllocFilters]);
const [confirmDelete, setConfirmDelete] = useState<{ single?: AllocationWithDetails; ids?: string[] } | null>(null);
const [batchStatusPicker, setBatchStatusPicker] = useState(false);
const [confirmBatchStatus, setConfirmBatchStatus] = useState<{ ids: string[]; status: string } | null>(null);
@@ -405,12 +423,7 @@ export function AllocationsClient() {
}
function clearAll() {
setFilterProjectId("");
setFilterResourceId("");
setFilterStatus("");
setHidePastProjects(false);
setHideCompletedProjects(false);
setHideDraftProjects(false);
setAllocFilters({ projectId: "", resourceId: "", status: "", hidePast: "false", hideCompleted: "false", hideDraft: "false" });
}
const chips = [
@@ -450,9 +463,7 @@ export function AllocationsClient() {
hideDraftProjects,
})
) {
setHidePastProjects(false);
setHideCompletedProjects(false);
setHideDraftProjects(false);
setAllocFilters({ hidePast: "false", hideCompleted: "false", hideDraft: "false" });
}
}, [
assignmentList.length,