From 1b7f20fc5ba1f4065e03c4cb7a1ea7d495355d5d Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Mon, 20 Jul 2026 12:18:52 +0200 Subject: [PATCH] refactor(ui): simplify provider wizard footer state --- .../use-provider-wizard-controller.test.tsx | 20 ++--- .../hooks/use-provider-wizard-controller.ts | 84 ++----------------- .../wizard/steps/connect-step.test.tsx | 11 ++- 3 files changed, 27 insertions(+), 88 deletions(-) diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx index 9f97379264..868b8e838d 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx @@ -271,7 +271,7 @@ describe("useProviderWizardController", () => { expect(refreshMock).not.toHaveBeenCalled(); }); - it("does not rerender when setting a semantically unchanged footer config", () => { + it("keeps the footer setter stable and uses replacement action callbacks", () => { // Given const onOpenChange = vi.fn(); const firstOnBack = vi.fn(); @@ -280,15 +280,13 @@ describe("useProviderWizardController", () => { const latestOnBack = vi.fn(); const latestOnSecondaryAction = vi.fn(); const latestOnAction = vi.fn(); - let renderCount = 0; - const { result } = renderHook(() => { - renderCount += 1; - - return useProviderWizardController({ + const { result } = renderHook(() => + useProviderWizardController({ open: true, onOpenChange, - }); - }); + }), + ); + const initialSetFooterConfig = result.current.setFooterConfig; const firstFooterConfig = { showBack: true, @@ -309,7 +307,6 @@ describe("useProviderWizardController", () => { act(() => { result.current.setFooterConfig(firstFooterConfig); }); - const renderCountAfterFirstUpdate = renderCount; // When act(() => { @@ -319,14 +316,15 @@ describe("useProviderWizardController", () => { onSecondaryAction: latestOnSecondaryAction, onAction: latestOnAction, }); - + }); + act(() => { result.current.resolvedFooterConfig.onBack?.(); result.current.resolvedFooterConfig.onSecondaryAction?.(); result.current.resolvedFooterConfig.onAction?.(); }); // Then - expect(renderCount).toBe(renderCountAfterFirstUpdate); + expect(result.current.setFooterConfig).toBe(initialSetFooterConfig); expect(result.current.resolvedFooterConfig).toMatchObject({ showBack: true, backLabel: "Back", diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts index 32d67de9ee..f59f47433a 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts @@ -1,13 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; -import { - type Dispatch, - type SetStateAction, - useEffect, - useRef, - useState, -} from "react"; +import { useEffect, useRef, useState } from "react"; import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls"; import { isCloud } from "@/lib/shared/env"; @@ -54,32 +48,6 @@ const EMPTY_FOOTER_CONFIG: WizardFooterConfig = { actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, }; -function isSameFooterConfig( - current: WizardFooterConfig, - next: WizardFooterConfig, -) { - return ( - current.showBack === next.showBack && - current.backLabel === next.backLabel && - Boolean(current.backDisabled) === Boolean(next.backDisabled) && - Boolean(current.showSecondaryAction) === - Boolean(next.showSecondaryAction) && - (current.secondaryActionLabel ?? "") === - (next.secondaryActionLabel ?? "") && - Boolean(current.secondaryActionDisabled) === - Boolean(next.secondaryActionDisabled) && - current.secondaryActionVariant === next.secondaryActionVariant && - current.secondaryActionType === next.secondaryActionType && - (current.secondaryActionFormId ?? "") === - (next.secondaryActionFormId ?? "") && - current.showAction === next.showAction && - current.actionLabel === next.actionLabel && - Boolean(current.actionDisabled) === Boolean(next.actionDisabled) && - current.actionType === next.actionType && - (current.actionFormId ?? "") === (next.actionFormId ?? "") - ); -} - interface UseProviderWizardControllerProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -114,14 +82,8 @@ export function useProviderWizardController({ const [orgCurrentStep, setOrgCurrentStep] = useState( ORG_WIZARD_STEP.SETUP, ); - const [footerConfig, setFooterConfigState] = + const [resolvedFooterConfig, setFooterConfig] = useState(EMPTY_FOOTER_CONFIG); - const footerConfigRef = useRef(EMPTY_FOOTER_CONFIG); - const footerActionCallbacksRef = useRef({ - onBack: () => footerConfigRef.current.onBack?.(), - onSecondaryAction: () => footerConfigRef.current.onSecondaryAction?.(), - onAction: () => footerConfigRef.current.onAction?.(), - }); const [providerTypeHint, setProviderTypeHint] = useState( null, ); @@ -161,8 +123,7 @@ export function useProviderWizardController({ ); setOrgCurrentStep(orgInitialData.targetStep); setOrgSetupPhase(orgInitialData.targetPhase); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); return; } @@ -185,8 +146,7 @@ export function useProviderWizardController({ ); setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(initialProviderType); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); return; @@ -197,8 +157,7 @@ export function useProviderWizardController({ setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }, [ @@ -235,8 +194,7 @@ export function useProviderWizardController({ setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); onOpenChange(false); @@ -265,26 +223,6 @@ export function useProviderWizardController({ setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH); }; - const updateFooterConfig: Dispatch> = ( - nextFooterConfig, - ) => { - const currentFooterConfig = footerConfigRef.current; - const resolvedNextFooterConfig = - typeof nextFooterConfig === "function" - ? nextFooterConfig(currentFooterConfig) - : nextFooterConfig; - footerConfigRef.current = resolvedNextFooterConfig; - - if (isSameFooterConfig(currentFooterConfig, resolvedNextFooterConfig)) { - return; - } - - setFooterConfigState({ - ...resolvedNextFooterConfig, - ...footerActionCallbacksRef.current, - }); - }; - const openOrganizationsFlow = () => { // AWS Organizations diverges from the credentials path the tour guides toward; end // it so it doesn't dangle on a step that no longer fits. No-op off-onboarding. @@ -292,8 +230,7 @@ export function useProviderWizardController({ resetOrgWizard(); setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }; @@ -302,8 +239,7 @@ export function useProviderWizardController({ resetOrgWizard(); setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); - footerConfigRef.current = EMPTY_FOOTER_CONFIG; - setFooterConfigState(EMPTY_FOOTER_CONFIG); + setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }; @@ -312,13 +248,11 @@ export function useProviderWizardController({ const docsLink = isProviderFlow ? getProviderHelpText(providerTypeHint ?? providerType ?? "").link : DOCS_URLS.AWS_ORGANIZATIONS; - const resolvedFooterConfig: WizardFooterConfig = footerConfig; const modalTitle = getProviderWizardModalTitle(mode); return { currentStep, docsLink, - footerConfig, handleClose, handleDialogOpenChange, handleTestSuccess, @@ -332,7 +266,7 @@ export function useProviderWizardController({ providerTypeHint, resolvedFooterConfig, setCurrentStep, - setFooterConfig: updateFooterConfig, + setFooterConfig, setOrgCurrentStep, setOrgSetupPhase, setProviderTypeHint, diff --git a/ui/components/providers/wizard/steps/connect-step.test.tsx b/ui/components/providers/wizard/steps/connect-step.test.tsx index 6401b9bd34..1544fbabb7 100644 --- a/ui/components/providers/wizard/steps/connect-step.test.tsx +++ b/ui/components/providers/wizard/steps/connect-step.test.tsx @@ -39,7 +39,7 @@ describe("ConnectStep", () => { useProviderWizardStore.getState().reset(); }); - it("does not publish a new footer config when form UI state is unchanged", async () => { + it("does not republish footer config for repeated unchanged form UI state", async () => { // Given const onFooterChange = vi.fn(); @@ -56,12 +56,19 @@ describe("ConnectStep", () => { // When act(() => { - capturedConnectAccountFormProps.current?.onUiStateChange?.({ + const unchangedUiState = { showBack: false, showAction: false, actionLabel: "Next", actionDisabled: true, isLoading: false, + }; + + capturedConnectAccountFormProps.current?.onUiStateChange?.( + unchangedUiState, + ); + capturedConnectAccountFormProps.current?.onUiStateChange?.({ + ...unchangedUiState, }); });