Repository navigation
Commit 4ccd790
fix(plugin-kanban): a card move on a read-only external object says the table is read-only, not that you lack permission (#30532)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuVWpRnE3E33m6sXj7FL8vXA)_
Fixes #30410
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:看板拖动卡片被服务端以
EXTERNAL_WRITE_FORBIDDEN(外部数据源只读表)拒绝时,提示改为「这张表是只读的:它的记录来自外部数据源,不接受修改。」,与表单、删除、行内编辑、日历等七处一致。
- 为什么改:以前提示「您无权执行此操作」,管理员会去翻权限集,而任何权限集都解不开这张表。
- 风险与回滚:只按错误码 EXTERNAL_WRITE_FORBIDDEN 换文案;作者标注的 userMessage 仍优先,其他 403
与其他错误一字不变;回滚即撤本 PR。
- 先看:ui/packages/plugin-kanban/src/ObjectKanban.tsx 卡片移动 catch 里的
refusal
三元式;ui/packages/plugin-kanban/src/ObjectKanban.markedRefusalToast.test.tsx
末尾新增的 describe。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-kanban/src/ObjectKanban.markedRefusalToast.test.tsx`,预期
8 个测试全绿。
- 需要你做的:无,席位落地。
## What changes
`ui/packages/plugin-kanban/src/ObjectKanban.tsx`, the card-move `catch`
(the `const refusal =` expression): `isExternalWriteRefusal(err)`
(`@object-ui/react`, added by #30403) is now asked after
`declaredUserMessage(err)` and before `isPermissionError(err)`, and
answers `form.readOnlyExternalSource`. This is byte-for-byte the order
of the calendar reschedule door in `ObjectCalendar.tsx`, the closest
sibling.
Order measured across #30403's seven doors (merge commit `a91409c42`):
every door that reads `declaredUserMessage` asks it FIRST, before
`isExternalWriteRefusal` (form page `refusalText`, record dialog in
`form.tsx`, stage change `stageRefusalLines`, calendar reschedule); the
three doors that ask `isExternalWriteRefusal` without a
`declaredUserMessage` (record header delete, list row delete, inline
edit) never read the marking at all. So an author-marked `userMessage`
still wins here too. Every other 403 keeps `errors.unauthorized`.
Out of scope, untouched (claim fence): the lane-refusal paths and every
other kanban refusal.
## Tests
New top-level `describe` in the existing harness
`ObjectKanban.markedRefusalToast.test.tsx` (real `onDragEnd`, error
built through `normaliseClientError`):
- an unmarked `403 EXTERNAL_WRITE_FORBIDDEN` (`code` plus
`details.code`) toasts the read-only line, and not the gate's
diagnostic;
- a marked one still toasts the marking.
The plain-403 control is the file's existing "keeps the localized
substitution for an UNMARKED 403", still green.
## Acceptance notes
- None beyond the fence above.
## Verification
<details><summary>Targeted tests and type-check at d9ca156</summary>
```
turbo run build --filter='@object-ui/plugin-kanban^...' : Tasks 52 successful, 52 total · VERDICT command-exit 0
pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-kanban/ : Test Files 85 passed (85) · Tests 551 passed (551) · VERDICT command-exit 0
pnpm --dir ui --filter @object-ui/plugin-kanban run type-check : VERDICT command-exit 0
```
</details>
<details><summary>Ablation at d9ca156</summary>
`node scripts/ablation-replace.mjs` replaced the anchor
`(isExternalWriteRefusal(err)` with `(false` (anchor x1 to x0, blob
618d26a to b7edb7d6b008), ran the harness file: `Tests 1 failed | 7
passed (8)`, the new read-only pin red with `Received: "You are not
authorized to perform this action."`. Restored: blob equals HEAD
(618d26a), `git diff HEAD` empty.
</details>
<details><summary>Gates</summary>
At d9ca156, `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 35 families; each was run with its
exit code recorded before any pipe:
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0 (first run exit 124 = my 300s timeout wrapper, rerun uncapped)
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
```
`dispatch-gates --ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
Also: `pnpm --dir ui run check:spec-symbols` exit 0; `pnpm --dir ui run
check:new-line-citations` exit 0 (0 new citations).
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgWn6zhNa4LL7VB1rtL
Co-authored-by: Claude <noreply@anthropic.com>1 parent d3b2aaf commit 4ccd790
3 files changed
Lines changed: 53 additions & 1 deletion
File tree
- .changeset
- ui/packages/plugin-kanban/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
1752 | 1753 | | |
1753 | 1754 | | |
1754 | 1755 | | |
1755 | | - | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
1756 | 1761 | | |
1757 | 1762 | | |
1758 | 1763 | | |
| |||
0 commit comments