feat(web): add object-view-active-view module for workspace page

This commit is contained in:
kumarabhirup 2026-03-02 18:35:41 -08:00
parent 3ed8872f16
commit a7a89a990e
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
2 changed files with 151 additions and 0 deletions

View File

@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import { emptyFilterGroup, type FilterGroup, type SavedView } from "@/lib/object-filters";
import { resolveActiveViewSyncDecision } from "./object-view-active-view";
function statusFilter(value: string): FilterGroup {
return {
id: "root",
conjunction: "and",
rules: [
{ id: "rule-status", field: "Status", operator: "is", value },
],
};
}
describe("resolveActiveViewSyncDecision", () => {
const importantView: SavedView = {
name: "Important",
filters: statusFilter("Important"),
columns: ["Name", "Status", "Owner"],
};
it("applies active view on initial load even when active view name already matches (prevents visible-but-unapplied bug)", () => {
const decision = resolveActiveViewSyncDecision({
savedViews: [importantView],
activeView: "Important",
currentActiveViewName: "Important",
currentFilters: emptyFilterGroup(),
currentViewColumns: undefined,
});
expect(decision).not.toBeNull();
expect(decision?.shouldApply).toBe(true);
expect(decision?.nextFilters).toEqual(statusFilter("Important"));
expect(decision?.nextColumns).toEqual(["Name", "Status", "Owner"]);
});
it("does not re-apply when name, filters, and columns are already aligned", () => {
const decision = resolveActiveViewSyncDecision({
savedViews: [importantView],
activeView: "Important",
currentActiveViewName: "Important",
currentFilters: statusFilter("Important"),
currentViewColumns: ["Name", "Status", "Owner"],
});
expect(decision).not.toBeNull();
expect(decision?.shouldApply).toBe(false);
});
it("re-applies when active view changes during refresh (keeps label and table state in sync)", () => {
const backlogView: SavedView = {
name: "Backlog",
filters: statusFilter("Backlog"),
columns: ["Name", "Status"],
};
const decision = resolveActiveViewSyncDecision({
savedViews: [importantView, backlogView],
activeView: "Backlog",
currentActiveViewName: "Important",
currentFilters: statusFilter("Important"),
currentViewColumns: ["Name", "Status", "Owner"],
});
expect(decision).not.toBeNull();
expect(decision?.shouldApply).toBe(true);
expect(decision?.nextActiveViewName).toBe("Backlog");
expect(decision?.nextFilters).toEqual(statusFilter("Backlog"));
expect(decision?.nextColumns).toEqual(["Name", "Status"]);
});
it("returns null when active view is missing or cannot be resolved", () => {
const missingActive = resolveActiveViewSyncDecision({
savedViews: [importantView],
activeView: undefined,
currentActiveViewName: "Important",
currentFilters: statusFilter("Important"),
currentViewColumns: ["Name", "Status", "Owner"],
});
const unknownActive = resolveActiveViewSyncDecision({
savedViews: [importantView],
activeView: "Unknown",
currentActiveViewName: "Important",
currentFilters: statusFilter("Important"),
currentViewColumns: ["Name", "Status", "Owner"],
});
expect(missingActive).toBeNull();
expect(unknownActive).toBeNull();
});
});

View File

@ -0,0 +1,59 @@
import {
type FilterGroup,
type SavedView,
emptyFilterGroup,
serializeFilters,
} from "@/lib/object-filters";
function areColumnsEqual(
a: string[] | undefined,
b: string[] | undefined,
): boolean {
if (!a && !b) {return true;}
if (!a || !b) {return false;}
if (a.length !== b.length) {return false;}
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) {return false;}
}
return true;
}
function areFiltersEqual(a: FilterGroup, b: FilterGroup): boolean {
return serializeFilters(a) === serializeFilters(b);
}
export type ActiveViewSyncDecision = {
shouldApply: boolean;
nextFilters: FilterGroup;
nextColumns: string[] | undefined;
nextActiveViewName: string;
};
export function resolveActiveViewSyncDecision(params: {
savedViews: SavedView[] | undefined;
activeView: string | undefined;
currentActiveViewName: string | undefined;
currentFilters: FilterGroup;
currentViewColumns: string[] | undefined;
}): ActiveViewSyncDecision | null {
const activeView = params.activeView;
if (!activeView) {return null;}
const view = (params.savedViews ?? []).find((candidate) => candidate.name === activeView);
if (!view) {return null;}
const nextFilters = view.filters ?? emptyFilterGroup();
const nextColumns = view.columns;
const nextActiveViewName = view.name;
const nameMismatch = params.currentActiveViewName !== nextActiveViewName;
const filterMismatch = !areFiltersEqual(params.currentFilters, nextFilters);
const columnMismatch = !areColumnsEqual(params.currentViewColumns, nextColumns);
return {
shouldApply: nameMismatch || filterMismatch || columnMismatch,
nextFilters,
nextColumns,
nextActiveViewName,
};
}