mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
filter persistence: url -> localStorage -> none (defaults)
This commit is contained in:
@@ -76,6 +76,69 @@ const parseIssueFilters = (search: string): IssuesTableFilters => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFilterStorageKey = (organisationId: number | null, projectId: number | null) => {
|
||||||
|
if (!organisationId || !projectId) return null;
|
||||||
|
return `sprint.issue-filters.${organisationId}.${projectId}`;
|
||||||
|
};
|
||||||
|
const FILTER_PARAM_KEYS = ["q", "status", "type", "assignee", "sprint", "sort"] as const;
|
||||||
|
|
||||||
|
const hasFilterParams = (search: string) => {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
return FILTER_PARAM_KEYS.some((key) => params.has(key));
|
||||||
|
};
|
||||||
|
|
||||||
|
const readStoredFilters = (storageKey: string): IssuesTableFilters | null => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as Partial<IssuesTableFilters> | null;
|
||||||
|
if (!parsed || typeof parsed !== "object") return null;
|
||||||
|
|
||||||
|
const statuses = Array.isArray(parsed.statuses) ? parsed.statuses.filter(Boolean) : [];
|
||||||
|
const types = Array.isArray(parsed.types) ? parsed.types.filter(Boolean) : [];
|
||||||
|
const assignees = Array.isArray(parsed.assignees) ? parsed.assignees.filter(Boolean) : [];
|
||||||
|
const query = typeof parsed.query === "string" ? parsed.query : "";
|
||||||
|
|
||||||
|
let sprintId: IssuesTableFilters["sprintId"] = "all";
|
||||||
|
if (parsed.sprintId === "none" || parsed.sprintId === "all") {
|
||||||
|
sprintId = parsed.sprintId;
|
||||||
|
} else if (typeof parsed.sprintId === "number" && !Number.isNaN(parsed.sprintId)) {
|
||||||
|
sprintId = parsed.sprintId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortValues: IssuesTableFilters["sort"][] = [
|
||||||
|
"newest",
|
||||||
|
"oldest",
|
||||||
|
"title-asc",
|
||||||
|
"title-desc",
|
||||||
|
"status",
|
||||||
|
];
|
||||||
|
const sort = sortValues.includes(parsed.sort as IssuesTableFilters["sort"])
|
||||||
|
? (parsed.sort as IssuesTableFilters["sort"])
|
||||||
|
: defaultIssuesTableFilters.sort;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultIssuesTableFilters,
|
||||||
|
query,
|
||||||
|
statuses,
|
||||||
|
types,
|
||||||
|
assignees,
|
||||||
|
sprintId,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeStoredFilters = (storageKey: string, filters: IssuesTableFilters) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(filters));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filtersEqual = (left: IssuesTableFilters, right: IssuesTableFilters) => {
|
const filtersEqual = (left: IssuesTableFilters, right: IssuesTableFilters) => {
|
||||||
if (left.query !== right.query) return false;
|
if (left.query !== right.query) return false;
|
||||||
if (left.sprintId !== right.sprintId) return false;
|
if (left.sprintId !== right.sprintId) return false;
|
||||||
@@ -125,8 +188,21 @@ export default function Issues() {
|
|||||||
const selectedOrganisation = useSelectedOrganisation();
|
const selectedOrganisation = useSelectedOrganisation();
|
||||||
const { data: membersData = [] } = useOrganisationMembers(selectedOrganisationId);
|
const { data: membersData = [] } = useOrganisationMembers(selectedOrganisationId);
|
||||||
const { data: sprintsData = [] } = useSprints(selectedProjectId);
|
const { data: sprintsData = [] } = useSprints(selectedProjectId);
|
||||||
const parsedFilters = useMemo(() => parseIssueFilters(location.search), [location.search]);
|
const filterStorageKey = useMemo(
|
||||||
const [issueFilters, setIssueFilters] = useState<IssuesTableFilters>(() => parsedFilters);
|
() => getFilterStorageKey(selectedOrganisationId, selectedProjectId),
|
||||||
|
[selectedOrganisationId, selectedProjectId],
|
||||||
|
);
|
||||||
|
const filterParamsPresent = useMemo(() => hasFilterParams(location.search), [location.search]);
|
||||||
|
const storedFilters = useMemo(() => {
|
||||||
|
if (filterParamsPresent || !filterStorageKey) return null;
|
||||||
|
return readStoredFilters(filterStorageKey);
|
||||||
|
}, [filterParamsPresent, filterStorageKey]);
|
||||||
|
const nextFilters = useMemo(() => {
|
||||||
|
if (filterParamsPresent) return parseIssueFilters(location.search);
|
||||||
|
if (storedFilters) return storedFilters;
|
||||||
|
return defaultIssuesTableFilters;
|
||||||
|
}, [filterParamsPresent, location.search, storedFilters]);
|
||||||
|
const [issueFilters, setIssueFilters] = useState<IssuesTableFilters>(() => nextFilters);
|
||||||
|
|
||||||
const organisations = useMemo(
|
const organisations = useMemo(
|
||||||
() => [...organisationsData].sort((a, b) => a.Organisation.name.localeCompare(b.Organisation.name)),
|
() => [...organisationsData].sort((a, b) => a.Organisation.name.localeCompare(b.Organisation.name)),
|
||||||
@@ -138,38 +214,13 @@ export default function Issues() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIssueFilters((current) => (filtersEqual(current, parsedFilters) ? current : parsedFilters));
|
setIssueFilters((current) => (filtersEqual(current, nextFilters) ? current : nextFilters));
|
||||||
}, [parsedFilters]);
|
}, [nextFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentParams = new URLSearchParams(location.search);
|
if (!filterStorageKey) return;
|
||||||
const nextParams = new URLSearchParams(location.search);
|
writeStoredFilters(filterStorageKey, issueFilters);
|
||||||
|
}, [filterStorageKey, issueFilters]);
|
||||||
if (issueFilters.query) nextParams.set("q", issueFilters.query);
|
|
||||||
else nextParams.delete("q");
|
|
||||||
|
|
||||||
if (issueFilters.statuses.length > 0) nextParams.set("status", issueFilters.statuses.join(","));
|
|
||||||
else nextParams.delete("status");
|
|
||||||
|
|
||||||
if (issueFilters.types.length > 0) nextParams.set("type", issueFilters.types.join(","));
|
|
||||||
else nextParams.delete("type");
|
|
||||||
|
|
||||||
if (issueFilters.assignees.length > 0) nextParams.set("assignee", issueFilters.assignees.join(","));
|
|
||||||
else nextParams.delete("assignee");
|
|
||||||
|
|
||||||
if (issueFilters.sprintId === "none") nextParams.set("sprint", "none");
|
|
||||||
else if (issueFilters.sprintId !== "all") nextParams.set("sprint", String(issueFilters.sprintId));
|
|
||||||
else nextParams.delete("sprint");
|
|
||||||
|
|
||||||
if (issueFilters.sort !== defaultIssuesTableFilters.sort) nextParams.set("sort", issueFilters.sort);
|
|
||||||
else nextParams.delete("sort");
|
|
||||||
|
|
||||||
if (currentParams.toString() === nextParams.toString()) return;
|
|
||||||
|
|
||||||
const search = nextParams.toString();
|
|
||||||
const nextUrl = `${location.pathname}${search ? `?${search}` : ""}`;
|
|
||||||
window.history.replaceState(null, "", nextUrl);
|
|
||||||
}, [issueFilters, location.pathname, location.search]);
|
|
||||||
|
|
||||||
const findById = <T,>(items: T[], id: number | null | undefined, getId: (item: T) => number) =>
|
const findById = <T,>(items: T[], id: number | null | undefined, getId: (item: T) => number) =>
|
||||||
id == null ? null : (items.find((item) => getId(item) === id) ?? null);
|
id == null ? null : (items.find((item) => getId(item) === id) ?? null);
|
||||||
|
|||||||
Reference in New Issue
Block a user