diff --git a/.changeset/ten-brooms-care.md b/.changeset/ten-brooms-care.md new file mode 100644 index 00000000000..d54dc024f98 --- /dev/null +++ b/.changeset/ten-brooms-care.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fix the Organization Security page briefly re-rendering its loading state when test-run data refetches after the initial page load. Test-run loading now only gates the first page load; afterward it stays at the table level. diff --git a/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx b/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx index bb9579ad215..a89ea8ef4c1 100644 --- a/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx +++ b/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx @@ -175,6 +175,32 @@ describe('useOrganizationEnterpriseConnection — test-runs gating', () => { expect(result.current.isLoading).toBe(false); }); + it('(d) latches after the first settle: a mid-session test-runs cold-load no longer raises the page-level isLoading', () => { + // (a) Initial load WITH a configured connection present: test-runs are part + // of the first skeleton, so the page-level isLoading gates on them. + connectionsState.data = [configuredConnection('ent_1')]; + testRunsState.isLoading = true; + + const { result, rerender } = renderHook(() => useOrganizationEnterpriseConnection()); + + expect(result.current.isLoading).toBe(true); + + // The first load settles (the test-runs probe resolves) → the skeleton drops. + testRunsState.isLoading = false; + rerender(); + expect(result.current.isLoading).toBe(false); + + // (b) A later (mid-session reconfigure) test-runs cold-load flips the + // underlying loading flag back on, but the page-level isLoading stays down — + // test-runs are strictly table-level after the first settle, never the + // global skeleton again. + testRunsState.isLoading = true; + rerender(); + expect(result.current.isLoading).toBe(false); + // The table-level signal is still available for the Test step to consume. + expect(result.current.testRuns.isLoading).toBe(true); + }); + it('keeps the global skeleton up while the connection source itself is loading', () => { connectionsState.isLoading = true; diff --git a/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts b/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts index c9cdd7090fb..77274bcc45a 100644 --- a/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts +++ b/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts @@ -146,6 +146,15 @@ export const useOrganizationEnterpriseConnection = (): UseOrganizationEnterprise } const hadInitialConnection = hadInitialConnectionRef.current === true; + // Has the very first page load — including the gated test-runs probe — settled + // at least once? Latches `true` exactly once and never flips back. Render-phase + // ref, matching `hadInitialConnectionRef` above: it records a one-time fact + // about load, it does not sync state to props. This is what scopes the + // test-runs term to the genuine initial load in the `isLoading` derivation + // before the return — so a mid-session reconfigure's cold test-runs load stays + // table-level and never re-raises the page skeleton. + const hasSettledRef = useRef(false); + // The test-runs source is relevant exactly when the connection is configured — // the same condition that makes the Test step reachable // (`hasMinimumConfiguration || isActive`). Deriving activation straight from @@ -317,16 +326,36 @@ export const useOrganizationEnterpriseConnection = (): UseOrganizationEnterprise [enterpriseConnection, hasSuccessfulTestRun], ); + // Test-runs gate the full skeleton only during the genuine FIRST page load, + // and only when a connection was present then (that case fetches them as part + // of the initial load). After the first settle the latch below freezes this + // contribution off: on the fresh-start path test-runs stay dormant until the + // connection is configured, and a later (re)configure's cold test-runs load + // surfaces as table-level loading, never the global skeleton. + const isInitialLoad = !hasSettledRef.current; + const isLoading = + isLoadingEnterpriseConnections || + isLoadingOrganizationDomains || + (isInitialLoad && hadInitialConnection && isLoadingTestRuns); + + // Latch settled once that first load — connections, domains, and (when gated) + // the test-runs probe — has finished. Evaluated AFTER `isLoading` so this + // render still counts as the initial load; from the next render on, test-runs + // no longer feed the page-level flag. + if ( + isInitialLoad && + !isLoadingEnterpriseConnections && + !isLoadingOrganizationDomains && + (!hadInitialConnection || !isLoadingTestRuns) + ) { + hasSettledRef.current = true; + } + return { user, session, organization, - // Test-runs gate the full skeleton only when a connection was present at - // first load — that case fetches them as part of the initial load. On the - // fresh-start path they stay dormant until the connection is configured, and - // landing on the test step then shows table-level loading, never the global - isLoading: - isLoadingEnterpriseConnections || isLoadingOrganizationDomains || (hadInitialConnection && isLoadingTestRuns), + isLoading, enterpriseConnection, organizationEnterpriseConnection, enterpriseConnectionMutations,