diff --git a/CHANGELOG.md b/CHANGELOG.md index 54903c47e07..855b97aefaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to ### Fixed +- Fix issue where back button must be pressed 3 times to go back once from the + Workflow canvas [#4812](https://github.com/OpenFn/lightning/issues/4812) - Fix `purge_deleted` Oban job crashing when a soft-deleted project has associated OAuth clients. The `project_oauth_clients` join rows are now cleaned up alongside the other project-scoped deletes in diff --git a/assets/js/react/lib/use-url-state.ts b/assets/js/react/lib/use-url-state.ts index a7a915e266b..348ced28414 100644 --- a/assets/js/react/lib/use-url-state.ts +++ b/assets/js/react/lib/use-url-state.ts @@ -102,6 +102,10 @@ class URLStore { const newURL = new URL(window.location.pathname, window.location.origin); newURL.search = newParams.toString(); newURL.hash = window.location.hash; + // Skip no-op writes so mount-time normalization doesn't stack + // duplicate browser history entries (a no-op pushState never notifies + // subscribers anyway, due to the guard in updateParams). + if (newURL.href === window.location.href) return; history.pushState({}, '', newURL); }; diff --git a/assets/test/react/lib/use-url-state.test.ts b/assets/test/react/lib/use-url-state.test.ts index 2464c4d7ec4..4dc5c68ae06 100644 --- a/assets/test/react/lib/use-url-state.test.ts +++ b/assets/test/react/lib/use-url-state.test.ts @@ -11,7 +11,7 @@ */ import { renderHook, act } from '@testing-library/react'; -import { describe, test, expect, beforeEach } from 'vitest'; +import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; import { useURLState } from '../../../js/react/lib/use-url-state'; @@ -201,6 +201,41 @@ describe('useURLState', () => { }); }); + describe('updateSearchParams - no-op writes', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('does not push a history entry when params are unchanged', () => { + history.replaceState({}, '', '/workflow?panel=run&job=abc'); + + const { result } = renderHook(() => useURLState()); + const pushSpy = vi.spyOn(history, 'pushState'); + + act(() => { + // Same values that are already in the URL -> a no-op + result.current.updateSearchParams({ panel: 'run', job: 'abc' }); + }); + + expect(pushSpy).not.toHaveBeenCalled(); + expect(window.location.search).toBe('?panel=run&job=abc'); + }); + + test('still pushes a history entry when a param actually changes', () => { + history.replaceState({}, '', '/workflow?panel=run'); + + const { result } = renderHook(() => useURLState()); + const pushSpy = vi.spyOn(history, 'pushState'); + + act(() => { + result.current.updateSearchParams({ panel: 'inspector' }); + }); + + expect(pushSpy).toHaveBeenCalledTimes(1); + expect(result.current.params.panel).toBe('inspector'); + }); + }); + describe('replaceSearchParams', () => { test('replaces all params with new ones (clears unspecified params)', () => { history.replaceState({}, '', '/workflow?panel=run&job=abc&step=5');