Repository navigation
Commit 9a0108c
fix(app-shell): a record locked by an approval is shown locked to a reader outside that approval (#26402)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi99vermfGkBLv9Vtsw2EJzVK)_
Fixes #25934
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:**
记录页在审批列表里找不到待审单时,再问一次服务器"这条记录锁了吗"(`client.approvals.getRecordLock`);答"锁了"就按锁定显示:编辑置灰、不给行内编辑、显示"审批锁定"横幅且不显示撤回按钮。
- **为什么改:** 能读记录但不在审批名单上的成员,读审批列表永远是空的,页面就当"没在审批",让他双击编辑,保存时才被服务器 409
拒绝(裁决 A,6422939157)。
- **风险与回滚:** 只改 app-shell 两个文件,不动导出类型和服务端;每次打开没有可见待审单的记录多一个 GET
请求。没有审批服务的部署该接口答 501,行为不变。回滚即 revert 本 PR。
- **先看:**
`ui/packages/app-shell/src/views/RecordDetailView.tsx`(`lockedForReader`
与撤回按钮的
`approvalIsSubmitter`)、`ui/packages/app-shell/src/hooks/useRecordApprovals.ts`(`startRead`
里的锁读取)、新测试 `RecordDetailView.approvalLock-25934.test.tsx`。
- **两分钟验证:** `pnpm --filter @object-ui/app-shell exec vitest run --root
../..
packages/app-shell/src/views/RecordDetailView.approvalLock-25934.test.tsx`,预期
4 个用例全绿。
- **需要你做的:** 无,席位落地。
## What changes
The approvals list (`GET /approvals/requests?object=…&recordId=…`)
serves a request only to its participants, so a member who can read a
record but is not on its approval read that list as `200 { data: [] }`
while a `lockRecord: true` approval held the record. The record page
took the answered empty read for "no approval is running" (rule 2 of
`resolveApprovalLock`) and offered inline edit, and the save answered
`409 RECORD_LOCKED`.
Ruling A (6422939157) added a lock signal every reader can see: `GET
/approvals/records/:object/:recordId/lock` and
`client.approvals.getRecordLock` (#26314, PR #26361), answering `{
locked }` only. This PR is the console half.
- `useRecordApprovals` (app-shell): when the list read finds no pending
request, the shared read also calls
`client.approvals.getRecordLock(object, recordId)` through the mounted
adapter's client (no hand-rolled URL), and the result carries
`recordLock: { locked } | null`. A pending request the viewer can see
still decides and the lock is not read. With no adapter mounted, the
lock read is skipped. A lock read that never answered (`501` without the
approvals service) leaves `null`; a later failure keeps the last answer,
as the rows are kept.
- `RecordDetailView` (lock region only): `{ locked: true }` from the
record lock is fed to `resolveApprovalLock` as its open approval. That
is rule 1's input exactly, since the server answers `true` only when a
pending request from a node whose `lockRecord` is not `false` exists. So
the page reads locked and pending, Edit is disabled and `canEdit` is
false. No lock answer means no open approval, so rule 2 is unchanged for
a composition without the approvals service. For a lock known only from
the record lock, `approvalIsSubmitter` is `false`: the reader cannot
read the request, and its submitter always can, so the band offers no
recall lever.
- No change to `ui/packages/react` (`resolveApprovalLock` /
`ApprovalLockInput` need no new input, because the signal is rule 1's
input) or to `ui/packages/plugin-detail` (the `DetailView` band computes
from the host's threaded `locked` / `approvalPending` /
`approvalIsSubmitter`, it does not compute its own lock input). No sdui
record is written: app-shell is not a registry package, and the
per-package records are retired (#26367).
- `Clause-②: no`: the claim graded it `yes (widening)` provisionally, on
the assumption that `ApprovalLockInput` would gain an input. It does
not. The one type that gains a member, the hook's result interface, is
not reachable from `@object-ui/app-shell`'s entry: `src/index.ts` does
not re-export `useRecordApprovals` and the interface is not exported.
This was measured on the source entry. The app-shell `dist` was not
built in this container.
## Tests
- New
`ui/packages/app-shell/src/views/RecordDetailView.approvalLock-25934.test.tsx`
(same probe as `RecordDetailView.approvalLock-12220`, record with no
`approval_status` mirror, like showcase):
- a non-participant (list `200` empty, lock `{ locked: true }`) gets
`locked`, `approvalPending`, `canEdit: false`, `approvalIsSubmitter:
false`, and the lock is read with the page's object and record id;
- lock `{ locked: false }` opens editable;
- lock read refused `501` (no approvals service) opens editable, the
answered empty read is not locked;
- a participant's pending `lock_record: false` row decides (pending,
editable) and the lock is not read.
- The objectui#12220 tests are unchanged and green:
`RecordDetailView.approvalLock-12220.test.tsx`,
`useRecordApprovals.answered-12220.test.tsx` (and
`resolveApprovalLock-12220.test.ts` /
`DetailView.approvalReadAnswered-12220.test.tsx`, whose packages this
diff does not touch). No test edit was forced.
## Acceptance notes
- One extra `GET` per record open where the list finds no pending
request. It goes through the adapter's authenticated, settle-counted
client fetch.
- The lock read starts with the list read on mount, on `refresh()`, and
again when the mounted adapter changes (`lockReader` is a `useCallback`
over `[adapter]`, since 40ca935). The console's `AdapterProvider`
mounts the adapter before the record page reads.
## Verification
- **Premises (re-run before any edit, origin/main `61c07986`).** 1:
`InlineEditContext.tsx:107` rule 2 holds. 2: `resolveApprovalLock({` at
`RecordDetailView.tsx:1664` and `DetailView.tsx:1716`. 3: the only lock
source was the list read, `useRecordApprovals.ts:480`. 6:
`lifecycle-hooks.ts` beforeUpdate `:538`,
`lockedError(recordLockRefusal` `:633`, refusal text `:294`. 7:
`lock_record?: boolean` at `approval-service.ts:336`. 9: PR #26053
merged (`054652bf`), so the serial condition is met. Line numbers
drifted, and every substance held.
- **Final head `7d11d10e`** (main merged after #26384; 40ca935 drops an
unused `act` import, the Lint UI red, and the hook's render-time ref
write): one locked run after a fresh install: `turbo run build
--filter='@object-ui/app-shell^...'` 63/63, `turbo lint
--filter=@object-ui/app-shell` 0 errors, app-shell type-check exit 0, 82
neighbouring files / 554 tests, `VERDICT command-exit 0`. **Earlier, on
`fca25b0b`, one locked run:** `turbo run build
--filter='@object-ui/app-shell^...'` (64/64), then `pnpm --filter
@object-ui/app-shell type-check` (src and `tsconfig.test.json`), then 4
files: the new test, `RecordDetailView.approvalLock-12220`,
`useRecordApprovals.answered-12220`,
`RecordDetailView.approvalRecallGate`. Test Files 4/4, Tests 25/25,
`VERDICT command-exit 0`.
- **Neighbours (pre-merge head `e0f9e560`):** all 71
`RecordDetailView.*.test.tsx`, the `useRecordApprovals*` hook tests,
`RecordApprovalsPanel*` and
`AppContent.recordSaveToastNamesRecord-26172`. Test Files 82/82, Tests
551/551, `VERDICT command-exit 0`. The full app-shell suite is left to
CI `Test UI`.
- **Ablation.** The fix was committed, then `useRecordApprovals.ts` and
`RecordDetailView.tsx` were restored to the base `61c07986` (on disk:
`getRecordLock` 3→0, `lockedForReader` 3→0). The new test's
non-participant case went red at `:235` (`expected 'false' to be
'true'`, `locked`). Both lock-read cases went red because
`getRecordLock` was never called. The participant control stayed green:
Tests 3 failed / 1 passed, `VERDICT command-exit 1`. Restored with `git
checkout HEAD --`: `git diff HEAD` empty, blob hashes equal HEAD's.
app-shell tests import the source directly, so no dist leg applies.
- **Gates on `7d11d10e`:** `dispatch-gates --commands` derived 34 (main
moved), all 34 exit 0, 0 NOT-MEASURED. **On `fca25b0b`:** derived 35.
All 35 exit 0, `check:type-check-debt` included. `--ran`: 35 derived, 35
run, 0 NOT-MEASURED, 0 UNRUN. Roster gate under `.changeset`:
`check-changeset-fixed.mjs` exit 0. All other families: this PR's
check-runs.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmQYqTcUW7GgPkfWCzw)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e0d6317 commit 9a0108c
4 files changed
Lines changed: 369 additions & 7 deletions
File tree
- .changeset
- ui/packages/app-shell/src
- hooks
- views
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
31 | 39 | | |
32 | 40 | | |
33 | 41 | | |
| 42 | + | |
34 | 43 | | |
35 | 44 | | |
36 | 45 | | |
| |||
278 | 287 | | |
279 | 288 | | |
280 | 289 | | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
281 | 302 | | |
282 | 303 | | |
283 | 304 | | |
| |||
415 | 436 | | |
416 | 437 | | |
417 | 438 | | |
| 439 | + | |
| 440 | + | |
418 | 441 | | |
419 | 442 | | |
420 | 443 | | |
| |||
423 | 446 | | |
424 | 447 | | |
425 | 448 | | |
| 449 | + | |
426 | 450 | | |
427 | 451 | | |
428 | 452 | | |
| |||
467 | 491 | | |
468 | 492 | | |
469 | 493 | | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
470 | 497 | | |
471 | | - | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
472 | 504 | | |
473 | 505 | | |
474 | 506 | | |
| |||
484 | 516 | | |
485 | 517 | | |
486 | 518 | | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
487 | 533 | | |
488 | 534 | | |
489 | 535 | | |
490 | 536 | | |
491 | 537 | | |
| 538 | + | |
492 | 539 | | |
493 | 540 | | |
494 | 541 | | |
| |||
543 | 590 | | |
544 | 591 | | |
545 | 592 | | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
546 | 601 | | |
547 | 602 | | |
548 | 603 | | |
| |||
565 | 620 | | |
566 | 621 | | |
567 | 622 | | |
568 | | - | |
| 623 | + | |
569 | 624 | | |
570 | 625 | | |
571 | 626 | | |
572 | 627 | | |
573 | 628 | | |
574 | | - | |
| 629 | + | |
575 | 630 | | |
576 | 631 | | |
577 | 632 | | |
578 | 633 | | |
579 | | - | |
580 | | - | |
| 634 | + | |
| 635 | + | |
581 | 636 | | |
582 | 637 | | |
583 | 638 | | |
| |||
613 | 668 | | |
614 | 669 | | |
615 | 670 | | |
| 671 | + | |
616 | 672 | | |
617 | 673 | | |
618 | 674 | | |
0 commit comments