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