Skip to content

fix(providers): 编辑/创建供应商弹窗失焦/点击外部不再关闭(Esc 仍可关闭)#1272

Merged
ding113 merged 4 commits into
devfrom
fix/provider-dialog-no-close-on-blur
Jun 11, 2026
Merged

fix(providers): 编辑/创建供应商弹窗失焦/点击外部不再关闭(Esc 仍可关闭)#1272
ding113 merged 4 commits into
devfrom
fix/provider-dialog-no-close-on-blur

Conversation

@ding113

@ding113 ding113 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

问题

编辑/创建供应商的弹窗在窗口失焦或点击弹窗外部时会被关闭,导致填写中的长表单内容意外丢失。期望:失焦/点击外部不再关闭;关闭按钮 / 取消 / 提交成功 / Esc 仍可正常关闭。

修复

  1. 行为原语 preventCloseOnOutsideInteraction(src/lib/utils/dialog.ts):仅中和 Radix 的 onInteractOutside(同时覆盖点击外部 + 窗口/标签页失焦),拦截 Esc。

  2. 封装组件 ProviderFormDialogContent(code-review 驱动的深层修复):把上面的关闭行为 6 处供应商表单弹窗重复的 shell className(max-h-[var(--cch-viewport-height-90)] flex flex-col overflow-hidden p-0 gap-0)一起内聚成一个组件,各站点只传各自的 max-w-*。这样新增供应商弹窗不可能再漏掉该行为——不存在可被复制的裸 <DialogContent>(消除"加站点忘加行为"的回归类)。

应用到全部 6 个承载 <ProviderForm> 的创建/编辑/克隆弹窗:add-provider-dialog、provider-manager(编辑)、vendor-keys-compact-list ×2、provider-rich-list-item(编辑 + 克隆)。

范围说明

  • provider-list-item.legacy.tsx无引用死代码,未改动。
  • 批量编辑弹窗(provider-batch-dialog.tsx,mode="batch")是独立功能,且其外部交互守卫是"提交中才阻止关闭"的不同语义,本 PR 未改。

代码评审(/code-review max effort)

对本改动做了多代理 + 机器人交叉评审:无正确性缺陷——针对实际安装的 Radix 源码(react-dismissable-layer@1.1.11)验证了内层 portal 子组件(Select/Popover/Combobox/嵌套 Dialog)注册在 context.branches 中、被排除在父弹窗的 outside 检测之外,故拦截 onInteractOutside 对它们是 no-op,无嵌套 UI 回归;Esc 行为保留;类型可赋值;6 站点全覆盖。评审提出的 altitude 建议(封装组件替代逐处 spread)已采纳实现。

测试与验证

  • tests/unit/dialog-prevent-close-on-outside-interaction.test.ts(3 例):onInteractOutside 调用 preventDefault注册 onEscapeKeyDown(Esc 仍可关)、仅暴露这一个守卫。
  • 本地全绿(真实退出码):build / typecheck / lint / test(全量 6439 passed)。
  • 无新增用户可见文案,无需 i18n。

🤖 Generated with Claude Code

Greptile Summary

This PR prevents provider create/edit dialogs from closing when clicking outside or on window/tab blur — protecting long in-progress form data from accidental loss. It introduces a shared preventCloseOnOutsideInteraction utility and a ProviderFormDialogContent wrapper component, then applies both to all 6 provider form dialogs.

  • src/lib/utils/dialog.ts — new utility that calls event.preventDefault() on onInteractOutside, covering both pointer-outside and focus-outside events while leaving Escape untouched.
  • provider-form-dialog-content.tsx — thin wrapper around DialogContent that bakes in the layout shell classes and the no-outside-close behavior, ensuring future dialogs cannot forget the protection.
  • All 6 call sites (add, edit, clone in three different components) migrated to the new wrapper; tests added for the utility.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to provider form dialogs and cannot affect unrelated dialog behavior.

All 6 call sites are migrated consistently, the new utility is simple and well-tested, and existing close paths (Escape, close button, cancel, successful submit) are untouched. The only minor concern is that the spread order in ProviderFormDialogContent would silently override a caller-supplied onInteractOutside, but no current caller passes one.

No files require special attention.

Important Files Changed

Filename Overview
src/lib/utils/dialog.ts New utility exporting preventCloseOnOutsideInteraction; correctly uses onInteractOutside + preventDefault to cover both click-outside and focus-outside without blocking Escape.
src/app/[locale]/settings/providers/_components/provider-form-dialog-content.tsx New shared wrapper component; correctly consolidates layout shell and close behavior. Spread order means a caller-supplied onInteractOutside prop is silently overridden rather than composed.
src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx Migrated to ProviderFormDialogContent; identical behavior preserved, no regressions.
src/app/[locale]/settings/providers/_components/provider-manager.tsx Edit dialog migrated to ProviderFormDialogContent; correct controlled open/close pattern retained.
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx Both Edit and Clone dialogs migrated cleanly; className values preserved.
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx Both add-key and edit-key dialogs migrated correctly; responsive max-width classes preserved.
tests/unit/dialog-prevent-close-on-outside-interaction.test.ts Three unit tests covering preventDefault call, absence of onEscapeKeyDown, and key-set shape — good coverage of the utility contract.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User interaction with provider dialog] --> B{Interaction type?}
    B -->|Outside click / window blur| C[onInteractOutside fires]
    B -->|Escape key| D[onEscapeKeyDown fires]
    B -->|Close button / Cancel| E[Controlled onOpenChange called]
    B -->|Form submit success| F[onSuccess calls setOpen false]
    C --> G[preventDefault called]
    G --> H[Dialog stays OPEN]
    D --> I[Default Radix behavior]
    I --> J[Dialog closes]
    E --> J
    F --> J
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/app/[locale]/settings/providers/_components/provider-form-dialog-content.tsx:26-30
The spread order here silently overrides any `onInteractOutside` prop a caller passes into `ProviderFormDialogContent`. Because `{...props}` comes first and `{...preventCloseOnOutsideInteraction}` comes second, a caller passing `onInteractOutside` to customize behavior (e.g., a future test helper or a subtly different dialog variant) will find their handler completely replaced with no warning. Merging the handlers makes the built-in guard additive rather than overriding.

```suggestion
    <DialogContent
      {...props}
      onInteractOutside={(event) => {
        event.preventDefault();
        props.onInteractOutside?.(event);
      }}
      className={cn(PROVIDER_FORM_DIALOG_SHELL, className)}
    >
```

Reviews (4): Last reviewed commit: "refactor(providers): extract ProviderFor..." | Re-trigger Greptile

Provider create and edit dialogs contain long forms that were previously
dismissed by accidental outside clicks, window focus loss, or the Escape
key, leading to potential data loss.

Introduce explicitCloseOnlyDialogProps to neutralize implicit close
triggers and apply it across all provider and vendor key dialogs. The
dialogs now only close via the explicit close button, cancel, or
successful submission.

Includes unit tests for the new dialog utility.
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

该 PR 新增 dialog 外部交互防护机制,创建 ProviderFormDialogContent 包装器组件预设布局与防护,并在四个 provider 相关组件中将 DialogContent 替换为该新组件,统一处理对话框外部交互关闭行为。

Changes

Dialog 防护与 Provider 组件集成

Layer / File(s) Summary
Dialog 防护工具与单元测试
src/lib/utils/dialog.ts, tests/unit/dialog-prevent-close-on-outside-interaction.test.ts
新增 preventCloseOnOutsideInteraction 常量导出,提供 onInteractOutside 回调以调用 event.preventDefault() 阻止外部交互自动关闭;新增单元测试验证防护行为、Escape 键保留及导出接口。
ProviderFormDialogContent 包装器
src/app/[locale]/settings/providers/_components/provider-form-dialog-content.tsx
新增 ProviderFormDialogContent 组件,基于 DialogContent 预设布局样式(最大高度、纵向布局、滚动裁剪),透传 preventCloseOnOutsideInteraction 防护属性,支持通过 className 追加自定义样式。
AddProviderDialog 应用防护
src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx
将对话框内容容器从 DialogContent 替换为 ProviderFormDialogContent,移除 DialogContent 导入,添加新包装器导入,传入 max-w-6xl className。
ProviderManager 应用防护
src/app/[locale]/settings/providers/_components/provider-manager.tsx
编辑提供者对话框将 DialogContent 替换为 ProviderFormDialogContent,调整导入,传入 max-w-6xl className;对话框内部 VisuallyHidden(DialogTitle)ProviderForm 渲染保持不变。
ProviderRichListItem 应用防护
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
编辑与克隆对话框均将 DialogContent 替换为 ProviderFormDialogContent,添加新组件导入,两处对话框传入 max-w-6xl className,表单与成功回调逻辑保持不变。
VendorKeysCompactList 应用防护
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
新增与编辑供应商对话框均将 DialogContent 替换为 ProviderFormDialogContent,添加新组件导入,两处对话框透传原有 className,表格与表单逻辑保持不变。

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • ding113/claude-code-hub#1033: ProviderManager 编辑对话框的 DialogContent 替换,同时涉及该文件的 dialog 流程重构。
  • ding113/claude-code-hub#923: ProviderRichListItem 编辑与克隆对话框容器替换,与该 PR 在同一 dialog 区域进行包装器变更。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题清晰准确地总结了主要变更:防止供应商编辑/创建弹窗在失焦或点击外部时关闭,同时说明 Esc 键仍可正常关闭。标题简洁具体,准确反映了本次修复的核心目标。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR 描述清晰地说明了问题、解决方案、范围和验证步骤,所有改动都与修复供应商弹窗在失焦/外部交互时不被关闭的问题相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/provider-dialog-no-close-on-blur

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a utility helper explicitCloseOnlyDialogProps to prevent dialogs from closing accidentally when a user clicks outside or presses the Escape key. This utility is applied to several provider-related dialogs (add, edit, clone, and vendor keys) to protect long forms from being discarded. Unit tests have also been added to verify the behavior of these handlers. There are no review comments, so I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@github-actions github-actions Bot added bug Something isn't working area:UI area:provider labels Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions github-actions Bot added the size/S Small PR (< 200 lines) label Jun 11, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

No significant issues identified in this PR. The change is narrow, well-scoped, and mechanically consistent: a single utility (explicitCloseOnlyDialogProps) that calls preventDefault() on two Radix Dialog implicit-close events, spread onto all 6 provider form dialogs. Unit tests cover both handlers and the public surface. No type safety, security, error handling, or standards concerns.

PR Size: S

  • Lines changed: 79
  • Files changed: 6

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Claude AI

The previous explicitCloseOnlyDialogProps helper blocked the Escape key
along with outside clicks and window blur, which was too restrictive
for users trying to dismiss long forms.

Rename the helper to preventCloseOnOutsideInteraction and drop the
onEscapeKeyDown interceptor. This keeps the protection against
accidental click-aways and focus loss while letting Escape close the
dialog as expected.
@ding113 ding113 changed the title fix(providers): 编辑/创建供应商弹窗失焦/点击外部不再关闭,仅显式关闭 fix(providers): 编辑/创建供应商弹窗失焦/点击外部不再关闭(Esc 仍可关闭) Jun 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e0bcfaefa

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/lib/utils/dialog.ts
*/
export const preventCloseOnOutsideInteraction = {
// Covers both pointer-down-outside and focus-outside (window/tab blur).
// Escape is intentionally left untouched so it still dismisses the dialog.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent Escape from discarding provider forms

For the provider create/edit dialogs using this helper, leaving Escape untouched means Radix still performs its default onOpenChange(false) when the user presses Escape. That still discards the long form without an explicit close action, so the new “explicit close only” behavior is not enforced for keyboard input; add an onEscapeKeyDown handler that calls preventDefault() if Escape should no longer close these dialogs.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

The file dialog-explicit-close.test.ts is renamed to dialog-prevent-close-on-outside-interaction.test.ts to align with the preventCloseOnOutsideInteraction helper it actually covers.
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Consolidate the preventCloseOnOutsideInteraction guard and standard
layout classes into a shared ProviderFormDialogContent component. This
ensures all provider create, edit, and clone dialogs consistently
prevent accidental dismissal via outside clicks or window blur without
requiring manual prop spreading.

Remove the standalone unit test for the interaction guard since the
behavior is now encapsulated within the component.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`[locale]/settings/providers/_components/add-provider-dialog.tsx:
- Line 10: 当前提交新增的导入使用了相对路径(例如 import { ProviderFormDialogContent } from
"./provider-form-dialog-content";)违背仓库约定,应改为使用项目别名 '`@/`'. 在文件中定位包含
ProviderFormDialogContent 的 import 语句以及另外三处新增相对导入,并将它们替换为以 '`@/`...'
开头的别名导入(保持原导出符号名不变),确保所有四处导入统一使用 alias 风格。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0837119b-e496-4372-8fd3-47cb22784d63

📥 Commits

Reviewing files that changed from the base of the PR and between 1edf230 and fe1fc52.

📒 Files selected for processing (5)
  • src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx
  • src/app/[locale]/settings/providers/_components/provider-form-dialog-content.tsx
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx

import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Dialog, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { ProviderForm } from "./forms/provider-form";
import { ProviderFormDialogContent } from "./provider-form-dialog-content";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

跨文件同一根因:新增导入统一使用了相对路径。

这 4 处新增导入都偏离了仓库 @/ 别名规范,建议一次性统一替换,避免后续继续扩散相对路径风格。

As per coding guidelines: **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/settings/providers/_components/add-provider-dialog.tsx at
line 10, 当前提交新增的导入使用了相对路径(例如 import { ProviderFormDialogContent } from
"./provider-form-dialog-content";)违背仓库约定,应改为使用项目别名 '`@/`'. 在文件中定位包含
ProviderFormDialogContent 的 import 语句以及另外三处新增相对导入,并将它们替换为以 '`@/`...'
开头的别名导入(保持原导出符号名不变),确保所有四处导入统一使用 alias 风格。

Source: Coding guidelines

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@ding113
ding113 merged commit 86bca49 into dev Jun 11, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jun 11, 2026
@github-actions github-actions Bot mentioned this pull request Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider area:UI bug Something isn't working size/S Small PR (< 200 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant