feat(web): add object-view-active-view module for workspace page
This commit is contained in:
parent
3ed8872f16
commit
a7a89a990e
92
apps/web/app/workspace/object-view-active-view.test.ts
Normal file
92
apps/web/app/workspace/object-view-active-view.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
59
apps/web/app/workspace/object-view-active-view.ts
Normal file
59
apps/web/app/workspace/object-view-active-view.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user