Bytebase 选择列整格点击目标改造:React 表格 Checkbox 从 16px 到整单元格的可点击区域实现方案

Bytebase 选择列整格点击目标改造:React 表格 Checkbox 从 16px 到整单元格的可点击区域实现方案

【免费下载链接】bytebase Database governance built for humans and agents — controlling changes and access across every major database. 【免费下载链接】bytebase 项目地址: https://gitcode.com/GitHub_Trending/by/bytebase

本文基于 docs/superpowers/plans/2026-05-11-checkbox-full-cell-click-target.md 与其配套设计文档 docs/superpowers/specs/2026-05-11-checkbox-full-cell-click-target-design.md 展开,并结合当前仓库中五张 React 表格的实际源码状态进行验证与补充。

Bytebase 前端在 React 迁移过程中出现了一个交互回归:带选择列的表格在 48px 宽的单元格里只渲染了一个 16px 的 Checkbox,用户点击复选框与单元格边缘之间的空白区域时,事件会冒泡到行级 onClick,导致本想勾选却被导航到资源详情页。本文完整讲解该问题的根因、面向三种表格形态(column-driven / inline JSX / flex-div)的三类局部修复方案、防止双重触发的 stopPropagation 事件机制,以及 5 张表格(Databases、Instances、Database Revisions、Projects、Issues)逐任务的落地步骤与浏览器手动 QA 清单。读完你可以复现整套"整格即选择目标"的改造方法论,并将其套用到任何带行导航与行选择的 React 表格上。


1. 问题背景:16px 的 Checkbox 与 48px 的单元格之间的"点击陷阱"

1.1 回归的本质

设计文档(docs/superpowers/specs/2026-05-11-checkbox-full-cell-click-target-design.md)对问题给出了精确定义:

Selection columns across React tables render a 16px Checkbox inside a 48px-wide cell on a row whose onClick navigates away. Clicks on cell padding (between the checkbox and the cell edge) miss the checkbox's small <span> wrapper, bubble to the row, and navigate to the resource — instead of toggling selection.

即:选择列渲染了一个 16px 的 Checkbox,位于 48px 宽的单元格内,而行的 onClick 负责导航跳转。点击落在 Checkbox 与其周围 padding 之间时,事件绕过了复选框的 <span> 包裹层,冒泡到行级 onClick,触发页面跳转——而不是切换选中状态。Vue 版本将整个复选框列暴露为选择目标,React 移植后发生了回归("The Vue version exposed the entire checkbox column as the selection target; the React port regressed")。

该 Bug 最早是在 databases 表格上被报出的(Linear issue BYT-9447:"Checkbox area is sensitive, very easy to click on the database instead of checking the database"),但同一模式存在于所有 React 选择表格中。

1.2 受影响的五张表面

设计文档列出的受影响表面(含各自表格形态与当前行为):

#文件表格形态当前行为
1frontend/src/components/database/DatabaseTableView.tsxColumn-driven <Table>行导航;单元格 padding 点击也会导航
2frontend/src/routes/workspace/InstancesPage.tsx(内联 InstanceColumn[]Column-driven <Table>同 #1
3frontend/src/routes/project/database-detail/revision/DatabaseRevisionTable.tsx内联 JSX <Table>行导航;单元格 padding 点击也会导航
4frontend/src/components/IssueTable.tsxFlex <div> 行(无 <Table>行导航;Checkbox 周围 padding 点击也会导航
5frontend/src/components/ProjectTable.tsx内联 JSX <Table>单元格已有 onClick={e => e.stopPropagation()}——不导航,但点击 padding 不做任何事(不切换选中)

需要注意:实施计划文档(plan)中写的路径是 frontend/src/react/components/... 前缀,而当前仓库中这些文件实际位于 frontend/src/components/frontend/src/routes/。在仓库中执行该计划时请以实际路径为准,本文下文一律使用仓库中确认存在的真实路径。

1.3 明确不在范围内(已验证无 Bug 的表面)

设计文档明确划出范围:ProjectPlanDashboardPage(计划列表无选择列)、BatchQuerySelect、以及 plan dashboard 内的数据库选择器(其行点击即切换选中,无导航冲突)——这三处不存在该 Bug,无需改动。


2. 总体设计:同概念、三形态、局部修复

设计文档确定的核心策略是:

Three local fixes, one per table shape. Same conceptual change ("the entire select cell is the click target"), but the surfaces are different enough that a shared helper would obscure more than unify.

同一个概念变化("整个选择单元格就是点击目标"),按表格形态分别实现,不引入共享组件,因为三者的表面差异足够大,强行抽象共享 helper 反而会让代码更难懂。每次修复在调用点只有 2–4 行代码。

三种形态的分类与对应任务:

  • Shape A — column-driven(列驱动 <Table>:Task 1(DatabaseTableView)+ Task 2(InstancesPage)。列类型上新增 onCellClick / onHeaderClick 可选字段,在渲染循环中接线。
  • Shape B — inline JSX <Table>:Task 3(DatabaseRevisionTable)+ Task 4(ProjectTable)。没有列类型可扩展,直接在选择的 <TableHead> / <TableCell> 上挂 onClick
  • Shape C — flex-div 行:Task 5(IssueTable)。用点击目标 <div> 包裹 Checkbox

技术栈为 React、@base-ui/react、Tailwind CSS v4;关联 Linear 工单 BYT-9447


3. Shape A:column-driven 表格新增 onCellClick / onHeaderClick 列字段

3.1 为什么选择"列字段"方案

在 column-driven 表格中,列的渲染统一由 columns.map() 驱动,单元格与表头都由列定义生成。因此最自然的方式是在列类型上增加两个可选的事件字段,让选择列(以及未来可能的 quick-action 列)声明自己的整格点击行为,而不必为某个特殊列写特判。

3.2 第一步:扩展 DatabaseColumn 接口

计划要求为 DatabaseColumn 增加两个可选字段(对应 plan 中 Task 1 Step 1):

interface DatabaseColumn {
  key: string;
  title: React.ReactNode;
  defaultWidth: number;
  minWidth?: number;
  resizable?: boolean;
  sortable?: boolean;
  sortKey?: DatabaseTableSort["key"];
  cellClassName?: string;
  render: (database: Database) => React.ReactNode;
  /** Click handler for the entire body `<TableCell>` for this column.
   *  Set on the select column to make the whole cell toggle selection.
   *  Consumers should call `e.stopPropagation()` to prevent row navigation. */
  onCellClick?: (database: Database, e: React.MouseEvent) => void;
  /** Click handler for the entire header `<TableHead>` for this column.
   *  Mirror of `onCellClick` for the header row (e.g. select-all). */
  onHeaderClick?: (e: React.MouseEvent) => void;
}

仓库现状佐证:在 frontend/src/components/database/DatabaseTableView.tsx 的第 73–85 行,DatabaseColumn 接口中已存在 onCellClick?: (database: Database, e: React.MouseEvent) => void(第 83 行)与 onHeaderClick?: (e: React.MouseEvent) => void(第 84 行),与计划中的签名完全一致。

3.3 第二步:表头渲染循环中接线 onHeaderClick

在表头渲染循环中,<TableHead> 需接收新 handler,并在存在时附加 cursor-pointer

return (
  <TableHead
    key={col.key}
    sortable={col.sortable && sortable}
    sortActive={sortActive}
    sortDir={sort?.order ?? "asc"}
    onSort={
      col.sortable && sortable && colSortKey
        ? () => toggleSort(colSortKey)
        : undefined
    }
    resizable={col.resizable}
    onResizeStart={
      col.resizable ? (e) => onResizeStart(colIdx, e) : undefined
    }
    className={cn(col.onHeaderClick && "cursor-pointer")}
    onClick={col.onHeaderClick}
  >
    {col.title}
  </TableHead>
);

关键前提(已由源码验证):基础组件 TableHead 会先转发消费方的 onClick,再执行 onSort。见 frontend/src/components/ui/table.tsx 第 94–97 行:

onClick={(e) => {
  onClick?.(e);
  if (sortable) onSort?.();
}}

由于选择列不可排序,表头点击时只有 onHeaderClick 会执行,不会触发排序。

仓库现状佐证:DatabaseTableView.tsx 第 396–415 行的 <TableHead> 渲染已包含 className={cn(col.onHeaderClick && "cursor-pointer")}onClick={col.onHeaderClick}

3.4 第三步:行循环中接线 onCellClick

databases.map 内的 <TableCell> 渲染处:

{columns.map((col) => (
  <TableCell
    key={col.key}
    className={cn(
      "overflow-hidden",
      col.cellClassName,
      col.onCellClick && "cursor-pointer"
    )}
    onClick={col.onCellClick ? (e) => col.onCellClick!(db, e) : undefined}
  >
    {col.render(db)}
  </TableCell>
))}

仓库现状佐证:DatabaseTableView.tsx 第 511–525 行的 DatabaseRowView 中已实现完全一致的接线(col.onCellClick && "cursor-pointer" + 条件 onClick)。

3.5 第四步:选择列定义接入新字段

将选择列定义替换为同时携带三个行为来源——表头 Checkbox、onCellClickonHeaderClick

if (showSelection) {
  cols.push({
    key: "select",
    title: (
      <Checkbox
        checked={someSelected ? "indeterminate" : allSelected}
        onCheckedChange={toggleSelectAll}
        onClick={(e) => e.stopPropagation()}
      />
    ),
    defaultWidth: 48,
    onCellClick: (db, e) => {
      e.stopPropagation();
      toggleSelection(db.name);
    },
    onHeaderClick: (e) => {
      e.stopPropagation();
      toggleSelectAll();
    },
    render: (db) => (
      <Checkbox
        checked={selectedNames?.has(db.name) ?? false}
        onCheckedChange={() => toggleSelection(db.name)}
        onClick={(e) => e.stopPropagation()}
      />
    ),
  });
}

双层 stopPropagation 是关键:表头与行体内的 Checkbox 上的 onClick={(e) => e.stopPropagation()} 会在事件冒泡到父级 <TableHead> / <TableCell>onClick 之前消费掉点击,从而防止用户直接点击 Checkbox 时发生"双重切换"(一次来自 onCheckedChange,一次来自单元格 handler)。

仓库实现细节:当前仓库中 DatabaseTableView 的选择列实际采用了更进一步的形态——选择列没有进入 columns 数组,而是把表头与行单元格单独渲染:表头 <TableHead> 在第 375–388 行直接内联 onClicke.stopPropagation() + toggleSelectAll()),行内 <TableCell> 在第 496–510 行内联 onClicke.stopPropagation() + onToggleSelection(database.name))。两种写法效果等价,目的都是"整格即点击目标",且选择列被排除在 columns 之外是为了保持 columns 引用稳定(避免每次选中切换都使所有行重渲染),这一设计在组件注释(第 210–215 行)中有明确说明。

3.6 第五步:静态校验

pnpm --dir frontend fix
pnpm --dir frontend type-check

预期两条命令都成功。若 type-check 在新可选字段上报错,问题很可能出在 DatabaseTableView.tsxuseMemo 依赖列表——onCellClick 闭包捕获了 toggleSelection,需确认其依赖(selectedNamesonSelectedNamesChange)已出现在依赖数组中(计划编写时已存在,通常无需新增依赖)。

3.7 第六步:浏览器手动 QA(9 点标准清单)

由于该改动是点击行为变更,而仓库中渲染这些表格需要重度 mock(stores、router、i18n、proto 工具),测试脚手架成本高于价值,因此仓库前端约定为"浏览器手动 QA"(见 frontend/AGENTS.md:"For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete")。启动开发服务器:

pnpm --dir frontend dev

导航到 Settings → Databases(挂载 DatabaseTable 的页面),逐项验证:

  1. 直接点击行内复选框 → 该行切换选中,无导航;
  2. 点击复选框右侧约 20px 的单元格 padding(仍在 48px 单元格内)→ 该行切换选中,无导航;
  3. 点击复选框上方/下方的单元格 padding(仍在单元格内)→ 切换选中,无导航;
  4. 点击数据库名称 → 导航到数据库页面(既有行为保留);
  5. 直接点击表头复选框 → 切换当前页全选;
  6. 点击表头单元格 padding → 全选切换(不触发排序——选择列不可排序);
  7. 有一行已选中时点击表头 → 全部选中(indeterminate → 全选);
  8. 全部行已选中时点击表头 → 全部取消;
  9. 鼠标悬停在选择单元格上显示 pointer 光标(cursor-pointer)。

3.8 第七步:提交

git add frontend/src/components/database/DatabaseTableView.tsx
git commit -m "$(cat <<'EOF'
fix(react-databases): full-cell click target for selection column

Adds onCellClick/onHeaderClick to DatabaseColumn so the entire 48px
select cell toggles selection instead of just the 16px checkbox. Fixes
the regression where clicking near (but not on) the checkbox would
navigate to the database page.

BYT-9447
EOF
)"

4. Task 2:InstancesPage —— 同一机制应用到 InstanceColumn

InstancesPage 是另一张 column-driven 表格,拥有自己内联定义的 InstanceColumn[]。计划的处理方式与 Task 1 完全镜像:先给 InstanceColumn 接口加两个可选字段,再在表头/行渲染循环接线,最后更新选择列定义。

InstanceColumn 的扩展(注意实体类型为 Instance):

interface InstanceColumn {
  key: string;
  title: React.ReactNode;
  defaultWidth: number;
  minWidth?: number;
  resizable?: boolean;
  sortable?: boolean;
  sortKey?: string;
  cellClassName?: string;
  render: (instance: Instance) => React.ReactNode;
  /** Click handler for the body `<TableCell>` of this column. */
  onCellClick?: (instance: Instance, e: React.MouseEvent) => void;
  /** Click handler for the header `<TableHead>` of this column. */
  onHeaderClick?: (e: React.MouseEvent) => void;
}

表头接线与 Task 1 相同(className={cn(col.onHeaderClick && "cursor-pointer")} + onClick={col.onHeaderClick},保留 sortable/onSort/resizable/onResizeStart 既有属性),行单元格接线同样为 col.onCellClick && "cursor-pointer" + 条件 onClick

选择列定义(注意该列额外带 cellClassName: "px-4 py-2",与 DatabaseTableView 的 defaultWidth: 48 共同构成完整命中区):

{
  key: "select",
  title: (
    <Checkbox
      checked={someSelected ? "indeterminate" : allSelected}
      onCheckedChange={toggleSelectAll}
      onClick={(e) => e.stopPropagation()}
    />
  ),
  defaultWidth: 48,
  cellClassName: "px-4 py-2",
  onCellClick: (instance, e) => {
    e.stopPropagation();
    toggleSelection(instance.name);
  },
  onHeaderClick: (e) => {
    e.stopPropagation();
    toggleSelectAll();
  },
  render: (instance) => (
    <Checkbox
      checked={selectedNames.has(instance.name)}
      onCheckedChange={() => toggleSelection(instance.name)}
      onClick={(e) => e.stopPropagation()}
    />
  ),
},

QA 步骤复用 Task 1 Step 6 的 9 点清单,仅将 "database" 替换为 "instance"、页面路径替换为 Settings → Instances。提交信息为 fix(react-instances): full-cell click target for selection column(正文镜像说明,含 BYT-9447 引用)。

类型一致性要求(计划自审中明确):onCellClick / onHeaderClick 的签名在 DatabaseColumn(Task 1)与 InstanceColumn(Task 2)之间必须完全一致,仅实体类型不同;且不得重命名各文件已有的 toggleSelection / toggleSelectAll 等函数名。


5. Shape B:inline JSX 表格直接在 TableHead / TableCell 上挂 onClick

5.1 Task 3:DatabaseRevisionTable

该表格没有列类型可扩展,因此直接在选择的 <TableHead> / <TableCell> 上挂事件处理器。

升级表头

<TableHead
  className="w-12 cursor-pointer"
  onClick={(e) => {
    e.stopPropagation();
    toggleSelectAll();
  }}
>
  <Checkbox
    checked={someSelected ? "indeterminate" : allSelected}
    onCheckedChange={toggleSelectAll}
    onClick={(e) => e.stopPropagation()}
  />
</TableHead>

升级行内单元格

<TableCell
  className="w-12 cursor-pointer"
  onClick={(e) => {
    e.stopPropagation();
    toggleSelection(revision.name);
  }}
>
  <Checkbox
    checked={selectedNames.has(revision.name)}
    onCheckedChange={() => toggleSelection(revision.name)}
    onClick={(e) => e.stopPropagation()}
  />
</TableCell>

仓库现状佐证frontend/src/routes/project/database-detail/revision/DatabaseRevisionTable.tsx 第 62–74 行的表头与第 90–102 行的行单元格已完全按此形态实现(className="w-12 cursor-pointer" + e.stopPropagation() + toggleSelectAll() / toggleSelection(revision.name),内层 Checkbox 保留 onClick={(e) => e.stopPropagation()})。

计划还附带一个务实提示:需确认 someSelectedallSelected 已在组件作用域内(它们应在周边局部变量中);若只有 allSelected,则保留 indeterminate prop 原样,只更新包裹的 <TableHead> 与内层 onClick

QA 路径:进入任意数据库详情页 → Revisions 标签页,复用 9 点清单(将 Step 4 中的"database page"替换为"revision detail page")。

5.2 Task 4:ProjectTable —— 升级既有 stopPropagation 为"切换选中"

ProjectTable 的情况略有不同:其选择单元格本来就有 onClick={(e) => e.stopPropagation()}(点击不导航),但也没有切换选中(点击 padding 无效果);而表头(144–151 行区域)则完全没有单元格级 handler。因此该任务是"升级"而非"新增"。

升级后的行内单元格(关键点:尊重默认项目的 disabled 状态):

{showSelection ? (
  <TableCell
    className={cn(
      "w-12",
      !isDefault && "cursor-pointer"
    )}
    onClick={(e) => {
      e.stopPropagation();
      if (isDefault) return;
      handleToggleRow(project.name);
    }}
  >
    <Checkbox
      checked={isSelected}
      aria-label={t("common.select")}
      disabled={isDefault}
      onCheckedChange={() => handleToggleRow(project.name)}
      onClick={(e) => e.stopPropagation()}
      className="disabled:opacity-50"
    />
  </TableCell>
) : ...}

if (isDefault) return; 保留了既有 disabled 默认行为:点击默认项目行的选择单元格时,阻止行级 onClick(不导航)但不切换选中;cursor-pointer 在默认行上被抑制,使视觉提示与实际行为一致。内层 Checkbox 新增 onClick={(e) => e.stopPropagation()} 是为了与其他修复对称(现在单元格有自己的点击 handler,直接点击 Checkbox 时防止双重切换)。若 cn 尚未导入,需添加 import { cn } from "@/lib/utils";(先检查既有导入列表)。

升级后的表头

{showSelection ? (
  <TableHead
    className="w-12 cursor-pointer"
    onClick={(e) => {
      e.stopPropagation();
      handleSelectAll();
    }}
  >
    <Checkbox
      checked={allSelected}
      aria-label={t("common.select-all")}
      onCheckedChange={handleSelectAll}
      onClick={(e) => e.stopPropagation()}
    />
  </TableHead>
) : ...}

仓库现状佐证frontend/src/components/ProjectTable.tsx 第 187–201 行(表头,e.stopPropagation() + handleSelectAll(),Checkbox 带 aria-label={t("common.select-all")})与第 320–332 行(行单元格,className={cn("w-12", !isDefault && "cursor-pointer")} + if (isDefault) return; + onToggleRow(project.name))均已按此实现。

ProjectTable 的补充 QA(额外两条)

  1. 点击默认项目行的选择单元格 → 不切换(复选框保持禁用)、不导航,光标为默认光标(非 pointer);
  2. 点击默认项目的行内容 → 照常导航。

6. Shape C:IssueTable —— 用 self-stretch div 包裹 Checkbox

IssueTable 没有 <Table>,行是 flex <div> 布局(flex items-start gap-x-2 px-4 py-3),Checkbox 位于行顶部(mt-1),其下方留有大量空白 padding,点击这些区域当前会导航到 issue 详情页。

计划方案:用点击目标 <div> 包裹 Checkbox,纵向填满整行高度,横向不扩展(保持标题单元格可导航):

<div
  className="shrink-0 self-stretch pt-1 cursor-pointer"
  onClick={(e) => {
    e.stopPropagation();
    onToggleSelection();
  }}
>
  <Checkbox
    checked={selected}
    onCheckedChange={() => onToggleSelection()}
    onClick={(e) => e.stopPropagation()}
  />
</div>

设计要点:

  • self-stretch 覆盖父级 items-start 对该子项的影响,使包裹层无论行内容在哪里结束都填满整行高度,可点击区域 = 整行高度 × 16px 复选框宽度;
  • pt-1 替代原 Checkbox 上的 mt-1——保持相同的视觉顶部对齐,但垂直间距的责任从 Checkbox 转移到包裹层;
  • shrink-0 从内层 Checkbox 移到包裹层,防止包裹层自身收缩;
  • 包裹层宽度隐式来自 Checkbox 内容(16px),不添加 pr-*,以保留 checkbox 与 issue 标题之间既有的 gap-x-2,并让标题区域保持可导航。

仓库现状佐证frontend/src/components/IssueTable.tsx 第 710–722 行已实现为 <div className="shrink-0 self-stretch cursor-pointer" onClick={...onToggleSelection(issue.name)}> 包裹 <Checkbox> 的形态(与计划中的 pt-1 版本相比,当前实现直接在 flex 行内使用 shrink-0 self-stretch,点击区域纵向撑满整行,行为一致)。

QA 路径:My Issues(/my-issues)与 Project → Issues(/project/<id>/issues),验证:

  1. 直接点击行内复选框 → 切换选中,无导航;
  2. 点击复选框上方/下方的空白纵向条带(仍在复选框列内、标题前的 gap 之前)→ 切换选中,无导航;
  3. 点击 issue 标题 → 导航到 issue 页;
  4. 点击行主体(状态图标、标签等)→ 导航;
  5. 验证行高与视觉布局未因包裹层发生偏移(必要时与 main 分支对比)。

注意:IssueTable 没有表头 select-all——全选控件在父级选择工具栏(SelectionActionBar)上,因此本任务无需镜像表头。


7. 为什么不会双重触发:checkbox.tsx 的 span 包裹机制

整个方案中反复出现的 onClick={(e) => e.stopPropagation()}承重墙(load-bearing)。其底层机制在 frontend/src/components/ui/checkbox.tsx 第 77–86 行:

if (!onClick) return root;

return (
  <span
    className={cn("inline-flex align-middle", className)}
    onClick={onClick}
  >
    {root}
  </span>
);

即:当传入 onClick 时,Checkbox 组件会用 <span> 包裹 Checkbox.Root,该 span 的 onClick 会在点击从复选框按钮内部冒泡出来之前消费事件。

由此得到两条互斥的点击路径,各自恰好触发一次切换:

  • 直接点击 Checkbox 本身Checkbox.Root 触发 onCheckedChange → 事件冒泡到包裹 span → span 的 stopPropagation 消费事件 → 单元格 onClick 永不触发 → 单次切换
  • 点击单元格 padding → 无 Checkbox 参与 → 单元格 onClick 触发 → 切换 + stopPropagation 阻断行导航 → 单次切换

因此,每个修复中内层 Checkbox 上的既有 onClick={(e) => e.stopPropagation()} 必须保留,一旦缺失,直接点击复选框时 onCheckedChange 与父级单元格 handler 会同时执行、切换两次(先选中又被取消,表现为"点了没反应")。


8. Task 6:最终验证

8.1 全量前端门禁

pnpm --dir frontend check
pnpm --dir frontend type-check
pnpm --dir frontend test

预期三条命令全部成功。check 是 CI 等价校验(lint + format + 整理 import,且不修改文件);test 应通过——本改动未新增或修改任何测试文件,但 lint 配置可能对改动文件报问题。若 check 报 "needs format",先运行 pnpm --dir frontend fix 再重跑 check

8.2 跨表面手动 QA 总扫

在同一个浏览器会话中验证全部六处入口:

表面URL
Databases/setting/database
Instances/setting/instance
Database revisions打开任意数据库 → "Revisions" 标签页
Projects/setting/project
Issues(workspace)/my-issues
Issues(project)打开任意项目 → "Issues" 标签页

每个表面都执行"单元格 padding 点击测试":在距 Checkbox 约 20px、仍位于选择列内的位置点击,确认切换选中且不导航。

8.3 cursor-pointer 误报检查

pnpm --dir frontend check 会顺带运行分层扫描器(check-react-layering.mjs,见 frontend/scripts/check-react-layering.mjs)。本改动未引入新的 z-index 或 portal,扫描器应干净通过;若报问题,与本 PR 无关,确认后再继续。

8.4 收尾提交

若 QA 过程中发现小修正(漏了 cursor-pointer、TypeScript 收窄等),单独提交:

git status
git add <fixed-files>
git commit -m "fix(react-tables): post-QA polish for full-cell click target

BYT-9447"

无改动则跳过此提交。


9. 明确不做的事(Out of Scope)

设计文档与计划都明确划定了边界,这些"不做"同样重要:

  • 不抽取共享 <SelectCell> / SelectionTable 原语——三种形态差异太大、修复又太小,共享抽象弊大于利;
  • 不扩大 Checkbox 原语自身的命中区域——那会影响全应用所有 Checkbox(包括非表格场景),是一个更大的 UX 决策;
  • 不重构 InstancesPage 的内联 InstanceColumn[] 去与 DatabaseColumn 共享类型
  • 不改动 frontend/src/components/ui/checkbox.tsx——它暴露的 onClick={(e) => e.stopPropagation()} span 包裹行为是防双重触发的承重设计,保持原样。

计划自审中的占位符扫描确认:无任何 "TBD / TODO / implement later / add appropriate error handling" 残留,所有代码块完整。唯一已知待核验点:Task 3 中 DatabaseRevisionTable.tsxsomeSelected / allSelected / toggleSelectAll 名称基于第 64–65 行可见代码引用,若实际局部变量名不同,需就地适配并保持逻辑不变。


10. 落地现状与可复用经验

从当前仓库源码看,该计划已完整落地,五张表格的选择列目前均实现了"整格点击目标":

  • DatabaseTableView.tsxDatabaseColumn 携带 onCellClick / onHeaderClick(第 83–84 行),表头/行渲染循环接线(第 410–411、517–521 行),选择列表头与行单元格分别内联 toggleSelectAll / toggleSelection + stopPropagation(第 375–388、496–510 行);
  • DatabaseRevisionTable.tsx:表头(第 62–74 行)与行单元格(第 90–102 行)直接挂 w-12 cursor-pointer + onClick
  • ProjectTable.tsx:表头 handleSelectAll(第 187–201 行)、行单元格 handleToggleRow 且以 if (isDefault) return; 尊重默认项目禁用态(第 320–332 行);
  • IssueTable.tsxshrink-0 self-stretch cursor-pointer div 包裹 Checkbox(第 710–722 行);
  • checkbox.tsx:保持零改动,span 包裹机制(第 77–86 行)持续为防双重切换提供支撑。

这套方案的可复用要点可概括为:明确"整格命中"这一单一概念目标;按表格形态分别落点(列字段 / 直接挂事件 / flex 包裹);用内层 stopPropagation 保证 Checkbox 自身点击不双触发;用 cursor-pointer 让可点击区域可见;用 9 点清单覆盖边界(padding 点击、indeterminate 状态、disabled 行、导航保留)。任何"行可点击导航 + 列可勾选"的 React 表格,都可以直接套用这三类形状之一的修法。

【免费下载链接】bytebase Database governance built for humans and agents — controlling changes and access across every major database. 【免费下载链接】bytebase 项目地址: https://gitcode.com/GitHub_Trending/by/bytebase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值