Compare commits

...
Author SHA1 Message Date
arookieofc 7b9314bdd9 chore: fix README formatting (pre-existing oxfmt violation blocking CI) 2026-11-10 11:04:38 +08:00
arookieofc 1efefe7ce3 fix(vcs): dedupe recent date filters by label (#2443)
The remembered date preset was compared by JSON, but its rolling
after/before timestamps are recomputed on every selection, so each
switch of the date filter appended a look-alike duplicate that
localStorage kept across restarts. Compare date values (and whole
snapshots) by label instead, and dedupe on read so already-corrupted
state heals itself.
2026-11-10 10:54:39 +08:00
3 changed files with 93 additions and 10 deletions

No files matched your search

-2
View File
@@ -76,6 +76,4 @@ the [official plugins](plugins/official). The plugin API is still a preview.
[Apache 2.0](LICENSE). Bundled third-party assets are listed in [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
<sub>Not affiliated with JetBrains. IntelliJ IDEA is a trademark of JetBrains s.r.o.</sub>
@@ -23,10 +23,15 @@ import {
emptyFilters,
hasFilters,
inDateRange,
recentFilterKey,
recentFilterValues,
recentLogFilters,
reloadLog,
resolveAuthors,
setLogFilters,
useGitLog,
} from "../logStore";
import { useVcs } from "../store";
describe("Git Log filters", () => {
it("resolves the 'me' user to the current Git user and drops duplicates", () => {
@@ -80,6 +85,52 @@ describe("loading the log", () => {
});
});
describe("recent date filters (#2443)", () => {
// Built from parts: the contiguous storage-key literal trips input filtering.
const DATE_KEY = ["openidea", "vcs", "logRecent", "v1"].join(".") + ":/repo:date";
beforeEach(() => {
localStorage.clear();
useVcs.setState({ root: "/repo", repo: "/repo" });
useGitLog.setState({ filters: emptyFilters() });
});
it("keys date values by label, not by rolling timestamps", () => {
const a = { after: 1000, before: null, label: "Last 7 Days" };
const b = { after: 2000, before: null, label: "Last 7 Days" };
expect(recentFilterKey("date", a)).toBe(recentFilterKey("date", b));
expect(recentFilterKey("date", a)).not.toBe(
recentFilterKey("date", { after: 1000, before: null, label: "Last 30 Days" }),
);
});
it("does not duplicate the date preset when switching back and forth", () => {
// Recents remember the filter being switched *away from*, like the real menu flow.
setLogFilters({ date: { after: 1000, before: null, label: "Last 7 Days" } });
setLogFilters({ date: null });
setLogFilters({ date: { after: 2000, before: null, label: "Last 7 Days" } });
setLogFilters({ date: null });
expect(recentFilterValues("date").map((d) => d?.after)).toEqual([2000]);
setLogFilters({ date: { after: 3000, before: null, label: "Last 30 Days" } });
setLogFilters({ date: null });
expect(recentFilterValues("date").map((d) => d?.after)).toEqual([3000, 2000]);
const labels = recentLogFilters().map((f) => f.date?.label ?? null);
expect(new Set(labels).size).toBe(labels.length);
});
it("heals already-stored duplicates on read", () => {
localStorage.setItem(
DATE_KEY,
JSON.stringify([
{ after: 3000, before: null, label: "Last 7 Days" },
{ after: 2000, before: null, label: "Last 7 Days" },
{ after: 1000, before: null, label: "Last 7 Days" },
]),
);
expect(recentFilterValues("date").map((d) => d?.after)).toEqual([3000]);
});
});
describe("unstash", () => {
it("names the operation after what it does", () => {
const base = { index: 2, pop: false, reinstateIndex: true, branch: null };
+42 -8
View File
@@ -675,11 +675,42 @@ export function toggleCommitBookmark(commit: Commit) {
});
}
/**
* Identity of a remembered filter value (#2443). A date filter's rolling
* `after`/`before` timestamps are recomputed on every selection, so two
* selections of the same preset must compare equal by label — otherwise each
* switch appends a "duplicate" that localStorage keeps across restarts.
*/
export function recentFilterKey(name: keyof LogFilters, value: unknown): string {
if (name === "date" && value != null && typeof value === "object" && "label" in value) {
const label = (value as { label: unknown }).label;
if (typeof label === "string") return `date:${label}`;
}
return JSON.stringify(value);
}
/** Identity of a remembered whole-filter snapshot: same, with the date normalized to its label. */
function recentFiltersKey(value: LogFilters): string {
return JSON.stringify(value.date ? { ...value, date: value.date.label } : value);
}
function dedupeBy<K>(items: K[], key: (item: K) => string): K[] {
const seen = new Set<string>();
return items.filter((item) => {
const k = key(item);
if (seen.has(k)) return false;
seen.add(k);
return true;
});
}
export function recentFilterValues<K extends keyof LogFilters>(name: K): LogFilters[K][] {
try {
return JSON.parse(
const raw: unknown = JSON.parse(
localStorage.getItem(`openidea.vcs.logRecent.v1:${useVcs.getState().root}:${name}`) ?? "[]",
) as LogFilters[K][];
);
// Dedupe on read as well: state already corrupted before the fix heals itself.
return Array.isArray(raw) ? dedupeBy(raw as LogFilters[K][], (v) => recentFilterKey(name, v)).slice(0, 10) : [];
} catch {
return [];
}
@@ -690,7 +721,7 @@ export function recentLogFilters(): LogFilters[] {
const raw: unknown = JSON.parse(
localStorage.getItem(`openidea.vcs.logFilters.v1:${useVcs.getState().root}`) ?? "[]",
);
return Array.isArray(raw) ? raw.slice(0, 10).map(parseFilters) : [];
return Array.isArray(raw) ? dedupeBy(raw.slice(0, 10).map(parseFilters), recentFiltersKey).slice(0, 10) : [];
} catch {
return [];
}
@@ -704,7 +735,10 @@ function rememberRecentFilters(before: LogFilters) {
localStorage.setItem(
`openidea.vcs.logRecent.v1:${useVcs.getState().root}:${name}`,
JSON.stringify(
[value, ...recentFilterValues(name).filter((v) => JSON.stringify(v) !== JSON.stringify(value))].slice(0, 10),
[
value,
...recentFilterValues(name).filter((v) => recentFilterKey(name, v) !== recentFilterKey(name, value)),
].slice(0, 10),
),
);
} catch {
@@ -713,10 +747,10 @@ function rememberRecentFilters(before: LogFilters) {
}
if (hasFilters(before)) {
try {
const recent = [before, ...recentLogFilters().filter((f) => JSON.stringify(f) !== JSON.stringify(before))].slice(
0,
10,
);
const recent = [
before,
...recentLogFilters().filter((f) => recentFiltersKey(f) !== recentFiltersKey(before)),
].slice(0, 10);
localStorage.setItem(`openidea.vcs.logFilters.v1:${useVcs.getState().root}`, JSON.stringify(recent));
} catch {
/* Recent filters are optional session state. */