feat(frontend): persist page filters in query params

This commit is contained in:
2026-04-29 11:31:12 +03:30
parent 06c05ba8e9
commit 06d083c818
12 changed files with 680 additions and 345 deletions

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useSearchParams } from "react-router-dom";
import { CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
import { toast } from "sonner";
@@ -25,6 +26,11 @@ import { Input } from "../components/ui/input";
import { SearchableSelect } from "../components/ui/SearchableSelect";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
import {
readArrayParam,
readStringParam,
updateQueryParams,
} from "../lib/queryParams";
type EntryModalMode = "manual" | "edit" | null;
@@ -2009,9 +2015,10 @@ function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) {
}
export default function Timesheet() {
const { t, lang } = useTranslation();
const { activeWorkspace } = useWorkspace();
const isRtl = lang === "fa";
const { t, lang } = useTranslation();
const { activeWorkspace } = useWorkspace();
const [searchParams, setSearchParams] = useSearchParams();
const isRtl = lang === "fa";
const extendedTimesheet = (t.timesheet as {
deleteTitle?: string;
deleteConfirmMessage?: string;
@@ -2047,10 +2054,19 @@ export default function Timesheet() {
const [activeRunningEntry, setActiveRunningEntry] = useState<TimeEntry | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
const [filters, setFilters] = useState<TimeEntryFilters>(DEFAULT_ENTRY_FILTERS);
const [hasMoreHistory, setHasMoreHistory] = useState(false);
const searchQuery = readStringParam(searchParams, "search", "");
const filters = useMemo<TimeEntryFilters>(
() => ({
projectId: readStringParam(searchParams, "project", DEFAULT_ENTRY_FILTERS.projectId),
clientId: readStringParam(searchParams, "client", DEFAULT_ENTRY_FILTERS.clientId),
tagIds: readArrayParam(searchParams, "tags"),
startedAfter: readStringParam(searchParams, "from", DEFAULT_ENTRY_FILTERS.startedAfter),
startedBefore: readStringParam(searchParams, "to", DEFAULT_ENTRY_FILTERS.startedBefore),
}),
[searchParams],
);
const [hasMoreHistory, setHasMoreHistory] = useState(false);
const [nextOffset, setNextOffset] = useState<number | null>(0);
const [limit] = useState(20);
const [ticker, setTicker] = useState(Date.now());
@@ -2134,9 +2150,6 @@ export default function Timesheet() {
}, [activeWorkspace?.id, t.timesheet?.optionsError]);
useEffect(() => {
setSearchQuery("");
setDebouncedSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
setGroupedHistory([]);
setNextOffset(0);
setHasMoreHistory(false);
@@ -2163,10 +2176,14 @@ export default function Timesheet() {
(project) => project.id === filters.projectId && project.client?.id === filters.clientId,
);
if (!projectStillMatchesClient) {
setFilters((current) => ({ ...current, projectId: "" }));
}
}, [filters.clientId, filters.projectId, projects]);
if (!projectStillMatchesClient) {
setSearchParams(
(current) =>
updateQueryParams(current, { project: "" }, { project: "" }),
{ replace: true },
);
}
}, [filters.clientId, filters.projectId, projects, setSearchParams]);
const loadHistory = useCallback(async ({ offset = 0, append = false }: { offset?: number; append?: boolean } = {}) => {
if (!activeWorkspace?.id) return;
@@ -2523,14 +2540,53 @@ export default function Timesheet() {
}, []);
const handleApplyFilters = useCallback((nextFilters: TimeEntryFilters) => {
setFilters(nextFilters);
}, []);
setSearchParams(
(current) =>
updateQueryParams(
current,
{
project: nextFilters.projectId,
client: nextFilters.clientId,
tags: nextFilters.tagIds,
from: nextFilters.startedAfter,
to: nextFilters.startedBefore,
},
{
project: "",
client: "",
from: "",
to: "",
},
),
{ replace: true },
);
}, [setSearchParams]);
const handleClearFilters = useCallback(() => {
setSearchQuery("");
setSearchParams(
(current) =>
updateQueryParams(
current,
{
search: "",
project: "",
client: "",
tags: [],
from: "",
to: "",
},
{
search: "",
project: "",
client: "",
from: "",
to: "",
},
),
{ replace: true },
);
setDebouncedSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
}, []);
}, [setSearchParams]);
const handleLoadMore = useCallback(() => {
if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
@@ -2789,7 +2845,13 @@ export default function Timesheet() {
<TimesheetFilterBar
searchQuery={searchQuery}
filters={filters}
onSearchChange={setSearchQuery}
onSearchChange={(value) =>
setSearchParams(
(current) =>
updateQueryParams(current, { search: value }, { search: "" }),
{ replace: true },
)
}
onApply={handleApplyFilters}
onClearFilters={handleClearFilters}
projects={projects}