Unified · Split
| @@ -2,4 +2,4 @@ · 1 unmodified line | |||
| 2 | "executor": patch | 2 | "executor": patch |
| 3 | --- | 3 | --- |
| 4 | 4 | ||
| 5 | Show a clear, cancellable sign-in state and prevent cancelled OAuth requests from disrupting a retry. | 5 | Let users cancel sign-in from the connection form and prevent cancelled OAuth requests from disrupting a retry. |
| @@ -122,6 +122,11 @@ scenario( · 121 unmodified lines | |||
| 122 | await step("Sign in with a provider account without naming the connection", async () => { | 122 | await step("Sign in with a provider account without naming the connection", async () => { |
| 123 | await visit(page, `/integrations/${slug}?addAccount=1`); | 123 | await visit(page, `/integrations/${slug}?addAccount=1`); |
| 124 | await page.getByRole("tab", { name: "OAuth2", exact: true }).click(); | 124 | await page.getByRole("tab", { name: "OAuth2", exact: true }).click(); |
| 125 | // Attached previews can contain other compatible apps. Use this | ||
| 126 | // scenario's provider instance for the full callback and token exchange. | ||
| 127 | await page | ||
| 128 | .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") }) | ||
| 129 | .check(); | ||
| 125 | const opened = page.waitForEvent("popup"); | 130 | const opened = page.waitForEvent("popup"); |
| 126 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); | 131 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); |
| 127 | const popup = await opened; | 132 | const popup = await opened; |
| @@ -299,6 +304,11 @@ scenario( · 171 unmodified lines | |||
| 299 | await step("Start provider sign-in without entering a name", async () => { | 304 | await step("Start provider sign-in without entering a name", async () => { |
| 300 | await visit(page, `/integrations/${slug}?addAccount=1`); | 305 | await visit(page, `/integrations/${slug}?addAccount=1`); |
| 301 | await page.getByRole("tab", { name: "OAuth2", exact: true }).click(); | 306 | await page.getByRole("tab", { name: "OAuth2", exact: true }).click(); |
| 307 | // Attached previews can contain other compatible apps. Use this | ||
| 308 | // scenario's provider instance for the full callback and token exchange. | ||
| 309 | await page | ||
| 310 | .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") }) | ||
| 311 | .check(); | ||
| 302 | const opened = page.waitForEvent("popup"); | 312 | const opened = page.waitForEvent("popup"); |
| 303 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); | 313 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); |
| 304 | const popup = await opened; | 314 | const popup = await opened; |
| @@ -313,6 +323,11 @@ scenario( · 8 unmodified lines | |||
| 313 | ).toBe(true); | 323 | ).toBe(true); |
| 314 | expect(await cancel.isEnabled()).toBe(true); | 324 | expect(await cancel.isEnabled()).toBe(true); |
| 315 | await page.getByText("Continue in the sign-in window", { exact: true }).waitFor(); | 325 | await page.getByText("Continue in the sign-in window", { exact: true }).waitFor(); |
| 326 | expect( | ||
| 327 | await page.getByRole("tab", { name: "OAuth2", exact: true }).isVisible(), | ||
| 328 | "the authentication form stays visible during sign-in", | ||
| 329 | ).toBe(true); | ||
| 330 | expect(await page.getByRole("textbox", { name: /Display name/ }).isVisible()).toBe(true); | ||
| 316 | expect( | 331 | expect( |
| 317 | await page.getByRole("button", { name: "Connecting…", exact: true }).count(), | 332 | await page.getByRole("button", { name: "Connecting…", exact: true }).count(), |
| 318 | "waiting for provider consent must not leave a dead Connecting button", | 333 | "waiting for provider consent must not leave a dead Connecting button", |
| @@ -371,10 +386,17 @@ scenario( · 52 unmodified lines | |||
| 371 | try { | 386 | try { |
| 372 | await step("Cancel while the authorization response is still in flight", async () => { | 387 | await step("Cancel while the authorization response is still in flight", async () => { |
| 373 | await visit(page, `/integrations/${slug}?addAccount=1`); | 388 | await visit(page, `/integrations/${slug}?addAccount=1`); |
| 389 | await page | ||
| 390 | .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") }) | ||
| 391 | .check(); | ||
| 374 | const opened = page.waitForEvent("popup"); | 392 | const opened = page.waitForEvent("popup"); |
| 375 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); | 393 | await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click(); |
| 376 | const popup = await opened; | 394 | const popup = await opened; |
| 377 | const oldState = await started.promise; | 395 | const oldState = await started.promise; |
| 396 | expect( | ||
| 397 | await page.getByRole("textbox", { name: /Display name/ }).isVisible(), | ||
| 398 | "the connection form stays visible while sign-in is being prepared", | ||
| 399 | ).toBe(true); | ||
| 378 | await page.getByRole("button", { name: "Cancel sign-in", exact: true }).click(); | 400 | await page.getByRole("button", { name: "Cancel sign-in", exact: true }).click(); |
| 379 | await expect.poll(() => popup.isClosed()).toBe(true); | 401 | await expect.poll(() => popup.isClosed()).toBe(true); |
| 380 | const cancelled = page.waitForResponse((response) => | 402 | const cancelled = page.waitForResponse((response) => |
| @@ -100,15 +100,7 @@ import { · 99 unmodified lines | |||
| 100 | DropdownMenuItem, | 100 | DropdownMenuItem, |
| 101 | DropdownMenuTrigger, | 101 | DropdownMenuTrigger, |
| 102 | } from "./dropdown-menu"; | 102 | } from "./dropdown-menu"; |
| 103 | import { | 103 | import { ChevronDown, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from "lucide-react"; |
| 104 | ChevronDown, | ||
| 105 | EyeIcon, | ||
| 106 | EyeOffIcon, | ||
| 107 | LoaderCircleIcon, | ||
| 108 | PanelsTopLeftIcon, | ||
| 109 | PlusIcon, | ||
| 110 | XIcon, | ||
| 111 | } from "lucide-react"; | ||
| 112 | import { | 104 | import { |
| 113 | Dialog, | 105 | Dialog, |
| 114 | DialogContent, | 106 | DialogContent, |
| @@ -2749,49 +2741,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 2634 unmodified lines | |||
| 2749 | : "sm:max-w-xl", | 2741 | : "sm:max-w-xl", |
| 2750 | )} | 2742 | )} |
| 2751 | > | 2743 | > |
| 2752 | {signInPending ? ( | 2744 | {addingMethod && createCustomMethod ? ( |
| 2753 | <> | ||
| 2754 | <DialogHeader> | ||
| 2755 | <DialogTitle>Add connection · {integrationName}</DialogTitle> | ||
| 2756 | </DialogHeader> | ||
| 2757 | <div role="status" className="flex flex-col items-center px-4 py-8 text-center"> | ||
| 2758 | <div className="mb-5 flex size-12 items-center justify-center rounded-xl border border-border/70 bg-muted/30 text-foreground shadow-sm"> | ||
| 2759 | {oauthPopup.phase === "authorizing" ? ( | ||
| 2760 | <PanelsTopLeftIcon aria-hidden="true" className="size-5" /> | ||
| 2761 | ) : ( | ||
| 2762 | <LoaderCircleIcon | ||
| 2763 | aria-hidden="true" | ||
| 2764 | className="size-5 animate-spin motion-reduce:animate-none" | ||
| 2765 | /> | ||
| 2766 | )} | ||
| 2767 | </div> | ||
| 2768 | <p className="text-sm font-medium text-foreground"> | ||
| 2769 | {oauthPopup.phase === "authorizing" | ||
| 2770 | ? "Continue in the sign-in window" | ||
| 2771 | : oauthPopup.phase === "saving" | ||
| 2772 | ? "Finishing connection" | ||
| 2773 | : "Preparing sign-in"} | ||
| 2774 | </p> | ||
| 2775 | <DialogDescription className="mt-2 max-w-xs text-sm leading-relaxed"> | ||
| 2776 | {oauthPopup.phase === "authorizing" | ||
| 2777 | ? "If the window closed or sign-in stalled, cancel and try again." | ||
| 2778 | : oauthPopup.phase === "saving" | ||
| 2779 | ? "Sign-in complete. Updating your connections." | ||
| 2780 | : "The sign-in window will be ready shortly. You can cancel at any time."} | ||
| 2781 | </DialogDescription> | ||
| 2782 | </div> | ||
| 2783 | <DialogFooter className="border-t border-border/60 pt-4 sm:justify-between"> | ||
| 2784 | <Button type="button" variant="ghost" onClick={close}> | ||
| 2785 | Close | ||
| 2786 | </Button> | ||
| 2787 | {oauthPopup.phase !== "saving" ? ( | ||
| 2788 | <Button type="button" variant="outline" onClick={cancelSignIn}> | ||
| 2789 | Cancel sign-in | ||
| 2790 | </Button> | ||
| 2791 | ) : null} | ||
| 2792 | </DialogFooter> | ||
| 2793 | </> | ||
| 2794 | ) : addingMethod && createCustomMethod ? ( | ||
| 2795 | <> | 2745 | <> |
| 2796 | <DialogHeader className="border-b border-border/60 px-5 py-4"> | 2746 | <DialogHeader className="border-b border-border/60 px-5 py-4"> |
| 2797 | <DialogTitle className="text-base">Add authentication method</DialogTitle> | 2747 | <DialogTitle className="text-base">Add authentication method</DialogTitle> |
| @@ -3442,10 +3392,20 @@ function AddAccountModalView(props: AddAccountModalProps) { · 644 unmodified lines | |||
| 3442 | </p> | 3392 | </p> |
| 3443 | ) : null} | 3393 | ) : null} |
| 3444 | <DialogFooter> | 3394 | <DialogFooter> |
| 3395 | {signInPending ? ( | ||
| 3396 | <p role="status" className="flex-1 self-center text-xs text-muted-foreground"> | ||
| 3397 | {oauthPopup.phase === "authorizing" | ||
| 3398 | ? "Continue in the sign-in window" | ||
| 3399 | : oauthPopup.phase === "saving" | ||
| 3400 | ? "Finishing connection…" | ||
| 3401 | : "Preparing sign-in…"} | ||
| 3402 | </p> | ||
| 3403 | ) : null} | ||
| 3445 | <Button type="button" variant="ghost" onClick={close} disabled={submitting || ccBusy}> | 3404 | <Button type="button" variant="ghost" onClick={close} disabled={submitting || ccBusy}> |
| 3446 | {isOAuth ? "Close" : "Cancel"} | 3405 | {isOAuth ? "Close" : "Cancel"} |
| 3447 | </Button> | 3406 | </Button> |
| 3448 | {/* Footer action, in precedence order: | 3407 | {/* Footer action, in precedence order: |
| 3408 | - pending sign-in: cancel, or wait for an authorized connection to save; | ||
| 3449 | - transparent CIMD (no app registration): create/reuse a public | 3409 | - transparent CIMD (no app registration): create/reuse a public |
| 3450 | metadata-document client and start OAuth; | 3410 | metadata-document client and start OAuth; |
| 3451 | - transparent DCR (no picker): a single Connect that runs | 3411 | - transparent DCR (no picker): a single Connect that runs |
| @@ -3453,7 +3413,17 @@ function AddAccountModalView(props: AddAccountModalProps) { · 1 unmodified line | |||
| 3453 | - registering a BYO app: the form owns its own submit, no footer; | 3413 | - registering a BYO app: the form owns its own submit, no footer; |
| 3454 | - picked BYO OAuth app: Connect with OAuth / Connect (client creds); | 3414 | - picked BYO OAuth app: Connect with OAuth / Connect (client creds); |
| 3455 | - credential/no-auth method: Add connection. */} | 3415 | - credential/no-auth method: Add connection. */} |
| 3456 | {cimdActive ? ( | 3416 | {signInPending ? ( |
| 3417 | oauthPopup.phase === "saving" ? ( | ||
| 3418 | <Button type="button" loading> | ||
| 3419 | Finishing… | ||
| 3420 | </Button> | ||
| 3421 | ) : ( | ||
| 3422 | <Button type="button" variant="outline" onClick={cancelSignIn}> | ||
| 3423 | Cancel sign-in | ||
| 3424 | </Button> | ||
| 3425 | ) | ||
| 3426 | ) : cimdActive ? ( | ||
| 3457 | <Button | 3427 | <Button |
| 3458 | type="button" | 3428 | type="button" |
| 3459 | onClick={() => void handleCimdConnect()} | 3429 | onClick={() => void handleCimdConnect()} |