Repository navigation
Commit 5dcbdb4
feat(service-print): print a record on any print page of its object, not only its default record page (#26662)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XN1ALtdy52CrgiAPP8xVRiT)_
Fixes #26583
Clause-②: yes (widening)
Metadata-protocol: none
**No authorable key is added or changed** (no `packages/spec/**` in the
diff; the request body already carries `page`), so per triage 6440154784
this card grades **p2**.
## 维护者速读
- **改了什么**:服务端打印(`@objectstack/service-print`)现在可以把记录打印在该对象的**任意一个声明了
`print` 的记录页**上,不再只限默认记录页。默认记录页照旧走记录路由;其它打印页走控制台已发布的具名记录页路由
`…/record/ID/page/PAGE`。
-
**为什么改**:以前要打印报价,就得把报价的日常记录页改成打印文档,销售每天都会失去状态路径、亮点区、详情/相关标签页和动态(objectstack-ai/hotcrm#2312
因此停着)。维护者裁决 A(6439306670)。
- **风险与回滚**:低。只放宽一条服务端检查、换一个 URL;页面不是该对象的记录页、或没声明 `print` 仍然 400
拒绝,未知页名仍然 404。回滚即 revert 本 PR。
-
**先看**:`packages/services/service-print/src/print-service.ts`(`refuseUnlessPrintPage`、新的
`isRecordPageOf` 与
`printPageUrl`);`packages/qa/dogfood/test/print-service-named-page.dogfood.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/service-print exec vitest run
src/print-service.test.ts`,预期 50/50 通过,其中「not the default record page
prints on the console’s named record-page route」一条。
- **需要你做的**:无,席位落地。
## What changes
`PrintService.printRecordToFile` (and so `POST /api/v1/print/records`),
step ③ "the page":
- **Accepted:** any page that is a record page of the request's object
and declares `print`. The third refusal on `main`, "is not the default
record page of OBJECT (isDefault: true)", is gone.
- **The URL:** the object's default record page still renders on the
console's record route, `CONSOLE/apps/APP/OBJECT/record/ID`. Any other
print page renders on the console's named record-page route,
`CONSOLE/apps/APP/OBJECT/record/ID/page/PAGE`
(`RECORD_NAMED_PAGE_ROUTE`,
`ui/packages/app-shell/src/console/AppContent.tsx:280`), every segment
encoded on its own (`printPageUrl`).
- **Two loud refusals remain**, both `VALIDATION_ERROR` / 400, before
anything is read, minted or launched: the page is not a record page of
the object, or it declares no `print`. An unknown page name stays
`RESOURCE_NOT_FOUND` / 404.
**The predicate both sides use.** The console's named route renders the
named page only when `isRecordPageOf(named, objectName)` holds
(`ui/packages/app-shell/src/views/RecordDetailView.tsx:787–792`), and
that function is `p.type === 'record' && p.object === objectName`
(`ui/packages/react/src/hooks/usePageAssignment.ts:94–98`); any other
name gets a not-found state, never a fallback. The server's check is now
that exact reading (`isRecordPageOf` in `print-service.ts`, docblock
names the console's), so the server prints exactly the set the console
draws, no wider. It is a local copy because `service-print` cannot
import `@object-ui/react`, and adding a shared reading to
`@objectstack/spec` is outside this lane.
`content/docs/ui/print-pages.mdx`: the "A print page that is not the
default is refused" sentence is rewritten to say a record prints on any
print page of its object (and on which route), the example page drops
`isDefault: true`, and the refusals table row reads "not a record page
of the object (`type: 'record'`, that `object`), or declares no
`print`". The repo carries no translation of this page (`find content
-name 'print-pages*'` → the en file only).
Changeset: `@objectstack/service-print` patch, both declaration lines
line-initial.
## Why
To print a quote, an app had to make the quote's everyday record page
its printed document, and reps lost the status path, highlights,
Details/Related tabs and the activity feed on every quote
(objectstack-ai/hotcrm#2312, REQ-0008). The console's named record-page
route (objectui#12715) already draws a named record page for a record
and refuses any other name; only the server half refused. Ruling
6439306670, letter A.
## Tests
Red first on `main` (`c739de755c`), then green at head:
| Suite | On `main` | At head |
|---|---|---|
| `service-print` `src/print-service.test.ts` (tests only changed) | 3
failed / 47 passed: the named-route accept, the encoded named URL, the
typeless default page | 50 / 50 |
| dogfood `print-service-named-page.dogfood.test.ts` +
`print-service-archive.dogfood.test.ts` (stand-in browser, real showcase
boot) | named-page leg red: "The page 'showcase_project_brief' is not
the default record page of 'showcase_project' (isDefault: true)…" | 13 /
13 |
| dogfood `print-service-render.dogfood.test.ts` (real console bundle +
real Chromium, both placements, `OS_PAGE_REACT_ENABLED=on`, see
Acceptance notes) | the 2 named-page legs red with the same refusal; the
other 6 green | 8 / 8 |
The `main` legs ran with `print-service.ts` restored from `c739de755c`
(the dogfood imports the service from `src/`), restored afterwards by
`git checkout HEAD --` and proved by `git diff HEAD` empty and the blob
hash matching HEAD.
Unit rows (`print-service.test.ts`):
- `'a print page that is not the default record page'` moved from the
refusal table to an accept: it renders
`…/record/inv_1/page/invoice_document` and archives
`invoice_document-inv_1.pdf`.
- The default page still renders the plain record route (pinned), and a
name carrying `/` and a record id carrying a space are encoded as one
segment each.
- Refusal rows kept or added: a page of another object, a page with no
`print` (default and non-default), a default `type: 'home'` page, a
non-default `type: 'app'` print page, and a page with no `type` (default
and non-default).
Dogfood (new `print-service-named-page.dogfood.test.ts`, fixture in
`print-named-page.fixture.ts`): `showcase_project` keeps its working
default record page `showcase_project_detail` (slotted, no `print`); a
project brief that declares `print` is saved beside it through the
metadata save door (`protocol.saveMetaItem`). A project prints on the
brief through the named route and is archived on the project as the
requester; the working page (no `print`) and the brief asked for an
invoice (another object) are refused 400 with nothing minted.
**What the real render put on paper.** A one-off copy of the render leg
wrote both PDFs out; `pdftotext -layout`:
- the brief, printed on the named route: `Project brief` / `Brief` /
`PROJECT NAME STATUS` / `Compliance Audit On Hold` / `START DATE END
DATE` / `Oct 12 Nov 26` / `1/1`, so the console drew the brief for that
project, not a not-found state and not the default page;
- the invoice, on the record route: `ObjectStack Showcase Ltd · 示例有限公司`
/ `Invoice` / `INV-RENDER-local Draft` …, unchanged.
Other local runs at head: `pnpm --filter @objectstack/service-print
test` 8 files / 114 tests passed; `pnpm --filter
@objectstack/service-print typecheck` exit 0 (its `include` is `src`, so
the test files are compiled); `pnpm --filter @objectstack/dogfood
typecheck` exit 0; eslint `--no-inline-config` on the touched
service-print and dogfood files exit 0.
## Acceptance notes
- **`Clause-②` measured on the diff.** The accept set widens: a request
naming a non-default print page, refused 400 on `main`, now prints. One
shape moves the other way, stated rather than hidden: a page body in the
registry with **no `type` at all** that is the default record page and
declares `print` was accepted on `main` (the old check read
`defaultRecordPageObject`, which treats an absent `type` as `'record'`)
and is now refused 400, because the console's `isRecordPageOf` reads
`type` exactly. On `main` such a print did not print that page: the
console's record route filters with the same `isRecordPageOf`, so it
drew the synthesized default page instead. No authoring door produces
that body (`defineStack` parses with `PageSchema`'s declared default,
and the metadata save door and stored-row hydration fill it, #20101
`withDeclaredPageTypeDefault`); only a plugin calling
`registry.registerItem` with a raw body can. I declared `yes
(widening)`; if the seat reads that one shape as a narrowing, the arm is
the seat's to change.
- **The render dogfood is red on a default environment, at `main` too,
for a reason outside this card.** Without `OS_PAGE_REACT_ENABLED`, the
console's policy is `script-src 'self' 'nonce-…' 'wasm-unsafe-eval'` (no
`'unsafe-eval'`, `packages/cli/src/utils/console.ts:851–852`, off unless
set), and the print driver's
`page.waitForFunction(PRINT_READY_PREDICATE, …)`
(`chromium-driver.ts:303`) passes a string, which Chromium refuses to
evaluate: all 6 legs failed, including the untouched invoice leg, with
`The print engine failed at the local executable …:
page.waitForFunction: EvalError: Evaluating a string as JavaScript
violates the following Content Security Policy directive …`. CI skips
this file (no console bundle), so `main` has not seen it. Reported to
the seat as a defect for its own card; the readings above were taken
with `OS_PAGE_REACT_ENABLED=on`.
- `examples/app-showcase/src/ui/pages/invoice-document.page.ts:12–14`
still says the invoice's print page is its default record page "because
the console renders a record on its default record page, so that is the
page a record prints on". That reason is no longer the only option after
this PR (the page may stay the default by choice). It is a comment in a
private package, outside the claim's file surface: left for the seat.
## Verification
Head `25f758a5e`. Local subset derived by `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on this diff (89 commands), each run with
its exit code captured before any pipe, reconciled with `--ran`:
```text
✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 88 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
```
- 88 exit 0. Two of them first exited 3 on their prerequisite and were
re-run after meeting it: `check-plugin-teardown-shape.mjs --self-test`
(positive control `621a4876` fetched into the shallow clone; then `48
cases pass`), and `spec check:skill-examples` (after building
`@objectstack/client-react`; then `266 prose examples type-check`).
- **NOT MEASURED: `pnpm check:dual-build-cjs-loads`**, reason: it needs
every package's `dist` (`no packages/plugins/knowledge-memory/dist` …)
and this container built only the dogfood closure. CI runs it.
- Not local, CI's: `pnpm lint` repo-wide (eslint on the touched files:
exit 0), `test:repo` (the diff touches no `vitest.repo-tests.json`
project), the full dogfood battery (no `!` and no `(narrowing)` arm).
<details><summary>Every local command and its exit code</summary>
```text
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-adr-links.mjs :: exit 0
node scripts/check-adr-links.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-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: 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-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: 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/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: 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:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: 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
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint passes on the touched files (repo-wide
`pnpm lint` is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a, not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
lane's required Chinese summary).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kGuMFreHR7gCGkpxL)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 47b6392 commit 5dcbdb4
8 files changed
Lines changed: 333 additions & 29 deletions
File tree
- .changeset
- content/docs/ui
- examples/app-showcase/src/ui/pages
- packages
- qa/dogfood/test
- services/service-print/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
32 | | - | |
| 31 | + | |
33 | 32 | | |
34 | 33 | | |
35 | 34 | | |
| |||
40 | 39 | | |
41 | 40 | | |
42 | 41 | | |
43 | | - | |
44 | | - | |
45 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
46 | 51 | | |
47 | 52 | | |
48 | 53 | | |
| |||
153 | 158 | | |
154 | 159 | | |
155 | 160 | | |
156 | | - | |
| 161 | + | |
157 | 162 | | |
158 | 163 | | |
159 | 164 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
13 | | - | |
14 | | - | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
| |||
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
| 45 | + | |
41 | 46 | | |
42 | 47 | | |
43 | 48 | | |
| |||
180 | 185 | | |
181 | 186 | | |
182 | 187 | | |
| 188 | + | |
| 189 | + | |
183 | 190 | | |
184 | 191 | | |
185 | 192 | | |
| |||
243 | 250 | | |
244 | 251 | | |
245 | 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 | + | |
246 | 277 | | |
247 | 278 | | |
248 | 279 | | |
| |||
0 commit comments