From 74729f6cab8a58880ed49731862099e1ca4b91b4 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:45:00 +0530 Subject: [PATCH 1/4] feat: refresh organization members and roles on history restores --- resources/js/app.tsx | 2 + .../js/lib/organization-members-history.ts | 15 ++++++ .../Unit/OrganizationMembersHistory.test.tsx | 54 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 resources/js/lib/organization-members-history.ts create mode 100644 tests/Unit/OrganizationMembersHistory.test.tsx diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 96a0f2243b..9cf9590f7c 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -13,6 +13,7 @@ import { trackCommitPhotoPage } from '@/lib/commit-photo'; import { initializeCommitState } from '@/lib/commit-state'; import { trackCurrentPage } from '@/lib/current-page'; import { trackFollowRequests } from '@/lib/follow-requests'; +import { trackOrganizationMembersHistory } from '@/lib/organization-members-history'; import { pageTitle } from '@/lib/page-title'; import { trackPrefetchedPages } from '@/lib/prefetched-pages'; import { registerServiceWorker } from '@/lib/service-worker'; @@ -22,6 +23,7 @@ import '../css/app.css'; trackCurrentPage(); trackCommitPhotoPage(); +trackOrganizationMembersHistory(); trackVisualViewport(); trackAnalyticsEvents(); trackPrefetchedPages(); diff --git a/resources/js/lib/organization-members-history.ts b/resources/js/lib/organization-members-history.ts new file mode 100644 index 0000000000..f648651aa5 --- /dev/null +++ b/resources/js/lib/organization-members-history.ts @@ -0,0 +1,15 @@ +import { router } from '@inertiajs/react'; + +/** Another admin may change memberships or roles after this page was cached. */ +export function trackOrganizationMembersHistory(): VoidFunction { + return router.on('navigate', ({ detail: { type, page } }) => { + if (type !== 'history' || page.component !== 'organization-member/index') { + return; + } + + router.reload({ + only: ['organization', 'role', 'members'], + preserveScroll: true, + }); + }); +} diff --git a/tests/Unit/OrganizationMembersHistory.test.tsx b/tests/Unit/OrganizationMembersHistory.test.tsx new file mode 100644 index 0000000000..1509fbd456 --- /dev/null +++ b/tests/Unit/OrganizationMembersHistory.test.tsx @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { test, vi } from 'vite-plus/test'; + +type NavigationType = 'initial' | 'visit' | 'history'; +type Navigation = { + detail: { + type: NavigationType; + page: { component: string }; + }; +}; + +const reload = vi.fn(); +let navigate: ((event: Navigation) => void) | undefined; + +vi.doMock('@inertiajs/react', () => ({ + router: { + on: (event: string, listener: (event: Navigation) => void) => { + assert.equal(event, 'navigate'); + navigate = listener; + + return () => { + navigate = undefined; + }; + }, + reload, + }, +})); + +const { trackOrganizationMembersHistory } = await import( + '../../resources/js/lib/organization-members-history' +); + +test('refreshes organization members and roles only on browser history restoration', () => { + reload.mockClear(); + const stop = trackOrganizationMembersHistory(); + const go = (type: NavigationType, component: string) => + navigate?.({ detail: { type, page: { component } } }); + + go('initial', 'organization-member/index'); + go('visit', 'organization-member/index'); + go('history', 'log/show'); + assert.equal(reload.mock.calls.length, 0); + + go('history', 'organization-member/index'); + assert.equal(reload.mock.calls.length, 1); + assert.deepEqual(reload.mock.calls[0][0], { + only: ['organization', 'role', 'members'], + preserveScroll: true, + }); + + stop(); + go('history', 'organization-member/index'); + assert.equal(reload.mock.calls.length, 1); +}); From 63fdcbf225d936ea9097d771faf98636cffccecf Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:08:14 +0530 Subject: [PATCH 2/4] fix: refresh organization members after bfcache restoration --- .../js/lib/organization-members-history.ts | 26 +++++++++++++++---- .../Unit/OrganizationMembersHistory.test.tsx | 22 +++++++++++++++- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/resources/js/lib/organization-members-history.ts b/resources/js/lib/organization-members-history.ts index f648651aa5..1f6c507128 100644 --- a/resources/js/lib/organization-members-history.ts +++ b/resources/js/lib/organization-members-history.ts @@ -1,15 +1,31 @@ import { router } from '@inertiajs/react'; +import { currentPageComponent } from '@/lib/current-page'; /** Another admin may change memberships or roles after this page was cached. */ export function trackOrganizationMembersHistory(): VoidFunction { - return router.on('navigate', ({ detail: { type, page } }) => { - if (type !== 'history' || page.component !== 'organization-member/index') { - return; - } - + const refresh = (): void => { router.reload({ only: ['organization', 'role', 'members'], preserveScroll: true, }); + }; + + const stop = router.on('navigate', ({ detail: { type, page } }) => { + if (type === 'history' && page.component === 'organization-member/index') { + refresh(); + } }); + + const onPageShow = (event: PageTransitionEvent): void => { + if (event.persisted && currentPageComponent() === 'organization-member/index') { + refresh(); + } + }; + + window.addEventListener('pageshow', onPageShow); + + return () => { + stop(); + window.removeEventListener('pageshow', onPageShow); + }; } diff --git a/tests/Unit/OrganizationMembersHistory.test.tsx b/tests/Unit/OrganizationMembersHistory.test.tsx index 1509fbd456..53a605d31e 100644 --- a/tests/Unit/OrganizationMembersHistory.test.tsx +++ b/tests/Unit/OrganizationMembersHistory.test.tsx @@ -10,6 +10,8 @@ type Navigation = { }; const reload = vi.fn(); +let current = 'log/show'; + let navigate: ((event: Navigation) => void) | undefined; vi.doMock('@inertiajs/react', () => ({ @@ -26,12 +28,22 @@ vi.doMock('@inertiajs/react', () => ({ }, })); +vi.doMock('@/lib/current-page', () => ({ + currentPageComponent: () => current, +})); + const { trackOrganizationMembersHistory } = await import( '../../resources/js/lib/organization-members-history' ); test('refreshes organization members and roles only on browser history restoration', () => { reload.mockClear(); + const events = new Map void>(); + vi.stubGlobal('window', { + addEventListener: (name: string, listener: (event: { persisted: boolean }) => void) => + events.set(name, listener), + removeEventListener: (name: string) => events.delete(name), + }); const stop = trackOrganizationMembersHistory(); const go = (type: NavigationType, component: string) => navigate?.({ detail: { type, page: { component } } }); @@ -48,7 +60,15 @@ test('refreshes organization members and roles only on browser history restorati preserveScroll: true, }); + current = 'organization-member/index'; + events.get('pageshow')?.({ persisted: false }); + assert.equal(reload.mock.calls.length, 1); + events.get('pageshow')?.({ persisted: true }); + assert.equal(reload.mock.calls.length, 2); + stop(); + assert.equal(events.has('pageshow'), false); go('history', 'organization-member/index'); - assert.equal(reload.mock.calls.length, 1); + assert.equal(reload.mock.calls.length, 2); + vi.unstubAllGlobals(); }); From d8c59cc6bb5da2678a076bc6a1f0f122193a7106 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:18:18 +0530 Subject: [PATCH 3/4] style: keep updated source within project formatting conventions --- resources/js/lib/organization-members-history.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/js/lib/organization-members-history.ts b/resources/js/lib/organization-members-history.ts index 1f6c507128..5896313de7 100644 --- a/resources/js/lib/organization-members-history.ts +++ b/resources/js/lib/organization-members-history.ts @@ -11,13 +11,19 @@ export function trackOrganizationMembersHistory(): VoidFunction { }; const stop = router.on('navigate', ({ detail: { type, page } }) => { - if (type === 'history' && page.component === 'organization-member/index') { + if ( + type === 'history' && + page.component === 'organization-member/index' + ) { refresh(); } }); const onPageShow = (event: PageTransitionEvent): void => { - if (event.persisted && currentPageComponent() === 'organization-member/index') { + if ( + event.persisted && + currentPageComponent() === 'organization-member/index' + ) { refresh(); } }; From 15776f1e9a278b65e9527f88f60754f5bfa64917 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:20:44 +0530 Subject: [PATCH 4/4] fix: refresh auth identity and reload after account changes --- .../js/lib/organization-members-history.ts | 21 +++++- .../Unit/OrganizationMembersHistory.test.tsx | 74 ++++++++++++------- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/resources/js/lib/organization-members-history.ts b/resources/js/lib/organization-members-history.ts index 5896313de7..202292a42f 100644 --- a/resources/js/lib/organization-members-history.ts +++ b/resources/js/lib/organization-members-history.ts @@ -1,16 +1,35 @@ import { router } from '@inertiajs/react'; import { currentPageComponent } from '@/lib/current-page'; +const authUserId = (page: { props: Record }) => { + const auth = page.props.auth as + | { user?: { id: string | number } | null } + | undefined; + + return auth?.user?.id ?? null; +}; + /** Another admin may change memberships or roles after this page was cached. */ export function trackOrganizationMembersHistory(): VoidFunction { + let currentUserId: string | number | null = null; + const refresh = (): void => { + const previousUserId = currentUserId; + router.reload({ - only: ['organization', 'role', 'members'], + only: ['organization', 'role', 'members', 'auth'], preserveScroll: true, + onSuccess: (page) => { + if (authUserId(page) !== previousUserId) { + window.location.reload(); + } + }, }); }; const stop = router.on('navigate', ({ detail: { type, page } }) => { + currentUserId = authUserId(page); + if ( type === 'history' && page.component === 'organization-member/index' diff --git a/tests/Unit/OrganizationMembersHistory.test.tsx b/tests/Unit/OrganizationMembersHistory.test.tsx index 53a605d31e..11075b5a0d 100644 --- a/tests/Unit/OrganizationMembersHistory.test.tsx +++ b/tests/Unit/OrganizationMembersHistory.test.tsx @@ -5,14 +5,16 @@ type NavigationType = 'initial' | 'visit' | 'history'; type Navigation = { detail: { type: NavigationType; - page: { component: string }; + page: { + component: string; + props: { auth: { user: { id: string } } }; + }; }; }; const reload = vi.fn(); -let current = 'log/show'; - let navigate: ((event: Navigation) => void) | undefined; +let current = 'log/show'; vi.doMock('@inertiajs/react', () => ({ router: { @@ -27,7 +29,6 @@ vi.doMock('@inertiajs/react', () => ({ reload, }, })); - vi.doMock('@/lib/current-page', () => ({ currentPageComponent: () => current, })); @@ -36,39 +37,56 @@ const { trackOrganizationMembersHistory } = await import( '../../resources/js/lib/organization-members-history' ); -test('refreshes organization members and roles only on browser history restoration', () => { +test('refreshes members and identity on history and bfcache restore', () => { reload.mockClear(); const events = new Map void>(); + const reloadPage = vi.fn(); vi.stubGlobal('window', { + location: { reload: reloadPage }, addEventListener: (name: string, listener: (event: { persisted: boolean }) => void) => events.set(name, listener), removeEventListener: (name: string) => events.delete(name), }); - const stop = trackOrganizationMembersHistory(); - const go = (type: NavigationType, component: string) => - navigate?.({ detail: { type, page: { component } } }); - go('initial', 'organization-member/index'); - go('visit', 'organization-member/index'); - go('history', 'log/show'); - assert.equal(reload.mock.calls.length, 0); + try { + const stop = trackOrganizationMembersHistory(); + const go = (type: NavigationType, component: string) => + navigate?.({ + detail: { + type, + page: { component, props: { auth: { user: { id: '1' } } } }, + }, + }); - go('history', 'organization-member/index'); - assert.equal(reload.mock.calls.length, 1); - assert.deepEqual(reload.mock.calls[0][0], { - only: ['organization', 'role', 'members'], - preserveScroll: true, - }); + go('initial', 'organization-member/index'); + go('visit', 'organization-member/index'); + go('history', 'log/show'); + assert.equal(reload.mock.calls.length, 0); + + go('history', 'organization-member/index'); + assert.equal(reload.mock.calls.length, 1); + const options = reload.mock.calls[0][0]; + assert.deepEqual(options.only, [ + 'organization', 'role', 'members', 'auth', + ]); + assert.equal(options.preserveScroll, true); + + options.onSuccess({ props: { auth: { user: { id: '1' } } } }); + assert.equal(reloadPage.mock.calls.length, 0); + options.onSuccess({ props: { auth: { user: { id: '2' } } } }); + assert.equal(reloadPage.mock.calls.length, 1); - current = 'organization-member/index'; - events.get('pageshow')?.({ persisted: false }); - assert.equal(reload.mock.calls.length, 1); - events.get('pageshow')?.({ persisted: true }); - assert.equal(reload.mock.calls.length, 2); + current = 'organization-member/index'; + events.get('pageshow')?.({ persisted: false }); + assert.equal(reload.mock.calls.length, 1); + events.get('pageshow')?.({ persisted: true }); + assert.equal(reload.mock.calls.length, 2); - stop(); - assert.equal(events.has('pageshow'), false); - go('history', 'organization-member/index'); - assert.equal(reload.mock.calls.length, 2); - vi.unstubAllGlobals(); + stop(); + assert.equal(events.has('pageshow'), false); + go('history', 'organization-member/index'); + assert.equal(reload.mock.calls.length, 2); + } finally { + vi.unstubAllGlobals(); + } });