From db6860d40e5e90fb9da2e01a75b8c570e69c91a8 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Sat, 25 Jul 2026 16:52:24 -0500 Subject: [PATCH] Fix legacy stack saved view comparison --- .../saved-views/use-saved-views.svelte.ts | 40 +++++++++---------- .../saved-views/use-saved-views.test.ts | 27 +++++++++++++ .../src/routes/(app)/stack/+page.svelte | 1 + 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.svelte.ts index ee29b500ce..68e499e372 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.svelte.ts @@ -106,6 +106,25 @@ export function hasSavedColumnVisibility(columns: null | Record return columns != null; } +export function savedViewColumnsEqual( + a: ColumnVisibilityState | undefined, + b: null | Record | undefined, + defaultColumnVisibility: ColumnVisibilityState = {} +): boolean { + const normalize = (value: ColumnVisibilityState | null | undefined) => ({ ...defaultColumnVisibility, ...(value ?? {}) }); + const aEntries = Object.entries(normalize(a)).sort(([k1], [k2]) => k1.localeCompare(k2)); + const bEntries = Object.entries(normalize(b)).sort(([k1], [k2]) => k1.localeCompare(k2)); + + if (aEntries.length !== bEntries.length) { + return false; + } + + return aEntries.every(([k, v], i) => { + const bEntry = bEntries[i]; + return bEntry !== undefined && bEntry[0] === k && bEntry[1] === v; + }); +} + export function setSortQueryParam(queryParams: SavedViewQueryParams, value: null | string): void { if (supportsSortQueryParam(queryParams)) { queryParams.sort = value; @@ -256,7 +275,7 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur if ( options.getColumnVisibility && hasSavedColumnVisibility(view.columns) && - !columnsEqual(options.getColumnVisibility(), view.columns, options.defaultColumnVisibility) + !savedViewColumnsEqual(options.getColumnVisibility(), view.columns, options.defaultColumnVisibility) ) { return true; } @@ -364,25 +383,6 @@ function columnOrderEqual(a: ColumnOrderState | undefined, b: null | string[] | return aOrder.every((columnId, index) => columnId === bOrder[index]); } -function columnsEqual( - a: ColumnVisibilityState | undefined, - b: null | Record | undefined, - defaultColumnVisibility: ColumnVisibilityState = {} -): boolean { - const normalize = (value: ColumnVisibilityState | null | undefined) => ({ ...defaultColumnVisibility, ...(value ?? {}) }); - const aEntries = Object.entries(normalize(a)).sort(([k1], [k2]) => k1.localeCompare(k2)); - const bEntries = Object.entries(normalize(b)).sort(([k1], [k2]) => k1.localeCompare(k2)); - - if (aEntries.length !== bEntries.length) { - return false; - } - - return aEntries.every(([k, v], i) => { - const bEntry = bEntries[i]; - return bEntry !== undefined && bEntry[0] === k && bEntry[1] === v; - }); -} - function normalizeFilterDefinitions(value: null | string | undefined): string { if (!value) { return '[]'; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.test.ts index 0c1b301792..0e27a46c67 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.test.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/use-saved-views.test.ts @@ -14,6 +14,7 @@ import { hasMissingSavedViewSlug, hasSavedColumnOrder, hasSavedColumnVisibility, + savedViewColumnsEqual, type SavedViewQueryParams, setSortQueryParam, setTimeQueryParam, @@ -211,6 +212,32 @@ describe('useSavedViews', () => { expect(hasSavedColumnVisibility({ events: false })).toBe(true); }); + it('treats legacy visibility missing default-hidden columns as unchanged', () => { + // Arrange + const current = { project: false, summary: true, tags: false }; + const legacySaved = { summary: true }; + const defaults = { project: false, tags: false }; + + // Act + const result = savedViewColumnsEqual(current, legacySaved, defaults); + + // Assert + expect(result).toBe(true); + }); + + it('detects a changed column after applying default visibility', () => { + // Arrange + const current = { project: true, summary: true, tags: false }; + const legacySaved = { summary: true }; + const defaults = { project: false, tags: false }; + + // Act + const result = savedViewColumnsEqual(current, legacySaved, defaults); + + // Assert + expect(result).toBe(false); + }); + it('does not compare column order when a saved view omits or clears column order', () => { // Act & Assert expect(hasSavedColumnOrder(null)).toBe(false); diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte index 9389da988a..af9d735493 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte @@ -234,6 +234,7 @@ let showChart = $state(true); const savedViewsState = useSavedViews({ baseHref: resolve('/(app)/stack'), + defaultColumnVisibility: defaultStackColumnVisibility, defaultFilter: DEFAULT_FILTER, defaultTime: DEFAULT_TIME_RANGE, filterCacheKey,