Skip to content

feat(access): Identities 操作 modal + <Can> 权限门 (PR-10) - #29

Merged
ghbvf merged 5 commits into
developfrom
010-b2-identities-ops
May 29, 2026
Merged

ghbvf merged 5 commits into
developfrom
010-b2-identities-ops

Conversation

@ghbvf

@ghbvf ghbvf commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

Batch 2 · Access · Identities 的第二段(PR-10,操作) — stacked 于 #28 (PR-09),base 为 009-b2-identities-list。覆盖 #13 的 T203(create/edit/change-password modal + lock/unlock)· T205(动作挂 <Can> + 路由 meta.requiredAction)。

⚠️ Review/merge PR-09 (#28) 先;本 PR 的 diff 仅含 PR-10 增量。

做了什么

  • api/identities:createUser(POST) · patchUser(PATCH) · deleteUser · lockUser · unlockUser · changeUserPassword(POST /users/{id}/password)。edit 走 PATCH(email-only PUT update 的超集,UI 不另用 update)。
  • useIdentitiesStore:create/edit/lock/unlock/remove/changePassword mutation actions —— 与读操作相反,失败 re-throw(modal 内联展示并保持打开),成功后 await fetchList()(changePassword 不 refetch)。
  • ModalShell(access-private a11y 原语):hand-rolled focus-trap + ESC + backdrop + 焦点回归。jsdom 无 HTMLDialogElement.showModal,故不用原生 <dialog>;第二个 cell 复用时上提 @gocell/core。
  • IdentityFormModal(create/edit)· ChangePasswordModal · ConfirmDialog(lock/unlock/delete,role="alertdialog")。
  • lib/identityValidation:复用 validation.ts 的 EMAIL_RE/USERNAME_RE/pwChecks,返回 i18n key。
  • IdentitiesView:行操作列 + Create 按钮,每个动作挂 <Can>(fail-closed 隐藏);wire 三个 modal。
  • 路由:/access/identities 挂 meta.requiredAction='read' + requiredResource='identity' → guards.ts PDP fail-closed gate(PDP 后端未接通前整页拒绝,BR-004;设计的安全姿态)。

语义提示

change-password 契约要 oldPassword+newPassword 且返回 token pair(轮换会话)→ 自助改密语义;管理员强制重置走 patch{requirePasswordReset:true}。

涉及包

  • @gocell/access:api/store/3 modals + ModalShell/validators/View(无新 exports——modal/原语均包内私有)
  • @gocell/core:access.identities.{actions,form,password,confirm}.* i18n(zh-CN + en-US;@ 已转义规避 vue-i18n linked-message trap)
  • apps/web:路由 meta.requiredAction

a11y

ModalShell:role=dialog/alertdialog + aria-modal + aria-labelledby + focus-trap(Tab/Shift+Tab wrap)+ ESC + 焦点回归;表单 <label for> + aria-invalid + aria-describedby + role="alert"。

Test plan

  • pnpm -F @gocell/access test(194 通过)
  • pnpm -w typecheck(11 包)· pnpm -w lint(0)· pnpm -w test(37 files / 511 通过)
  • pnpm codegen && git diff --exit-code packages/contracts/src/(clean)
  • 整包覆盖率 ~97.5% lines(ModalShell focus-trap 98.5%)

Closes #13

ref: antdv-pro/antdv-pro src/pages/list/components/crud-table-modal.vue
ref: W3C WAI-ARIA APG dialog-modal pattern

🤖 Generated with Claude Code

ghbvf and others added 5 commits May 30, 2026 02:09
Batch 2 · Access · Identities 第二段(操作;#13 · T203 · T205),stacked 于 PR-09。

- api/identities:create/patch/delete/lock/unlock/changeUserPassword
  (edit 走 PATCH,是 email-only PUT update 的超集,UI 不另用 update)
- useIdentitiesStore:create/edit/lock/unlock/remove/changePassword
  mutation actions — 失败 re-throw(modal 内联展示并保持打开),成功
  await fetchList()(changePassword 不 refetch)
- components/ModalShell:a11y modal 原语(hand-rolled focus-trap + ESC +
  backdrop + 焦点回归;jsdom 无 HTMLDialogElement.showModal);第二个 cell
  复用时再上提 @gocell/core
- components/IdentityFormModal(create/edit)· ChangePasswordModal ·
  ConfirmDialog(lock/unlock/delete,role=alertdialog)
- lib/identityValidation:create/edit/change-password 校验(复用
  validation.ts 的 EMAIL_RE/USERNAME_RE/pwChecks,返回 i18n key)
- IdentitiesView:行操作列 + Create 按钮,每个动作挂 <Can>(fail-closed
  隐藏);wire 三个 modal
- apps/web 路由 /access/identities 挂 meta.requiredAction='read' +
  requiredResource='identity'(guards.ts PDP fail-closed gate)
- core i18n:access.identities.{actions,form,password,confirm}.* 双语
  (email 占位符 @ 转义;规避 vue-i18n linked-message trap)

access 194 通过、整包 ~97.5% lines;workspace 511 通过;contracts clean。

Closes #13

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6-reviewer 审查的 IN_SCOPE Cx1/Cx2 修复:

- a11y 对比度(P1):新增 themed `--err-strong` token(light 0.47 / dark 0.72),
  危险按钮改 `color:var(--bg)`+`bg:var(--err-strong)`(随主题反色,两套主题都过
  WCAG AA),危险行操作文字改 `--err-strong`(旧 `--err` 在白底仅 ~3.3:1)
- a11y(P1):ConfirmDialog role=alertdialog 补 `aria-describedby` → message;
  ModalShell 新增 `descriptionId` prop
- ModalShell 加固:watch `flush:'post'`(消除开模一帧内 Tab 逃逸的 listener 时序
  缝隙);Tab-trap 用 indexOf,panel/外部焦点(idx=-1)也环绕,焦点不再逃出;
  opener 用 `instanceof HTMLElement`;`els[0]!`/`els.at(-1)!` 去除 as 转型;
  FOCUSABLE 选择器补 summary/contenteditable
- ChangePasswordModal:校验收进 computed(对齐 IdentityFormModal,去掉每次渲染
  3× 重复校验)
- IdentitiesView:onConfirm 改显式三分支(lock/unlock/delete),去掉 catch-all
  fallthrough(防未来新增 action 静默落到 remove)
- IdentityFormModal:requirePasswordReset checkbox 补 id

未处理(→ 人工确认,见 PR/收尾):core 内业务 i18n(pre-existing,需 i18n 联邦
化)、ModalShell 上提 @gocell/core(文档化的"第二 cell 再提")、乐观更新替代
全量 refetch、背景 inert、tab 方向键、z-index/hover token 化、useId 多实例、
逐行 aria-label。

access 194 通过;typecheck/lint 全绿;覆盖率 ~97.4%(ModalShell 98.6%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…保留 + a11y/token 收口

应"能处理的就处理好",把可干净修复的 deferred 项落地(i18n 联邦化单独 PR):

- [#2 架构] ModalShell 从 @gocell/access 上提到 @gocell/core/components(纯 UI 原语,
  无业务);三个 modal 改 import { ModalShell } from '@gocell/core/components';
  spec 一并迁移到 core。core ./components 已有 exports,无新增入口。
- [#4 a11y] ModalShell 开启时对背景兄弟节点逐级 inert(panel→body 跳过自身链路),
  补 aria-modal 在非原生 dialog 下 NVDA/JAWS 虚拟光标可达背景的缺口;关闭/卸载还原。
- [#3 bug] useIdentitiesStore 新增 refreshLoaded():mutation 成功后按"已加载条数"
  重取(而非塌回第 1 页),不再丢失 loadMore 展开的页;新增 100 行场景测试。
- [#5 a11y] 行操作按钮加 username 作用域 aria-label(actionLabel 助手 +
  access.identities.actions.rowSuffix i18n),读屏不再"编辑/编辑/编辑"。
- [#6 token] 新增 --z-modal / --fg-hover token;ModalShell z-index 与 create/
  primary 按钮 hover 改用 token(去 z-index 魔法数 + oklch 字面量,PR-10 新代码)。
- [#7 复用] ConfirmDialog 加 cancelKey prop(默认 access.identities.confirm.cancel),
  解除与 identities 命名空间的耦合。

未做(理由):i18n 联邦化(#1,唯一被核查判 should-fix)—— 需迁移已合并的 PR-08
login/firstRun key + 改 apps/web i18n bootstrap,属独立横切重构,单独开 PR;useId
唯一 title-id(无现存 bug、单实例,且会冲掉 4 个 spec 的 #id 选择器)。

core 193 / access 186 通过;workspace 515 通过(useTheme 为既有并发 flake,隔离通过);
typecheck/lint 全绿;contracts 未触碰(codegen 另现 audit schema 上游漂移,与本 PR 无关,
属独立 codegen-sync)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/access/src/views/IdentitiesView.vue
#	packages/core/src/i18n/messages/en-US.ts
#	packages/core/src/i18n/messages/zh-CN.ts
/pr-review 29(6-reviewer 六维度)IN_SCOPE 修复:

- 设计/bug(P1):ChangePasswordModal `.cp__btn--primary` 与 ConfirmDialog
  `.confirm__btn--primary` / `--danger` 缺 hover 覆盖 → 被通用 :hover 的
  --bg-sunken 冲掉,primary 变「白字白底」不可见、danger 丢失红色。补
  variant hover(primary→--fg-hover,danger→--err-strong + brightness)
- perf(P1):IdentitiesView formatDate 把 Intl.DateTimeFormat 提到 setup 级
  单例(原每行每渲染重建,构造成本 ~10×)
- TS(P1):IdentityFormModal/ChangePasswordModal fieldError 返回 string(''=无),
  去掉模板 6 处 `as string` 强转
- TS(P1):onConfirm 补 else 的 never 穷举守门(未来新增 ConfirmAction 编译期报错)
- a11y(P1):行操作按钮 height 28px → min-height 30px(PRD 数字基线 30px 例外)
- a11y(P2):requirePasswordReset checkbox label 补 for(消除未用 id)
- 设计(P2):tokens [data-theme=dark] 补 --fg-hover(暗色改为变暗,否则 near-white
  → white hover 不可感知)

未做(理由):tab 方向键漫游(仅 1 个启用 tab,退化 tablist,待 Wave 2 服务账号
启用再补);role=alert 同次重复提交不重播(错误可见且首次已播报,edge);resultCount
英文复数(sr-only,vue-i18n 复数 API 风险 > 收益);ModalShell/Can tree-shaking
(Can 在 access 同被使用,无实际影响);useId 唯一 id(单实例无冲突);i18n 业务 key
在 core(既有,独立联邦化 PR)。

access 188 / core 193 通过;typecheck / lint 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf
ghbvf changed the base branch from 009-b2-identities-list to develop May 29, 2026 19:55
@ghbvf
ghbvf merged commit c57b413 into develop May 29, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Batch 2] Access · Identities

1 participant