Skip to content

Commit e7a6cc7

Browse files
feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names (#28682)
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbBLJwakjtd38ozmWiE7HN9i)_ Fixes #28470 ## 维护者速读 - **改了什么**:打开一条记录时,评论、动态、附件和关联行里引用记录的名称,都跟着 `POST /data/:object/:id/page` 一起返回;@ 提及候选人(`sys_user?top=50`)改为评论框第一次获得焦点时才读。打开记录不再单独发这些请求,也没有回退读取。 - **为什么改**:在 app-crm 的生产构建上实测,打开一个联系人除了 `/page` 之外还会发 `sys_comment`、`sys_activity`、`sys_user?top=50`,以及每个被引用对象一次 `?top=1` 名称查询,共 8 个多余请求。 - **风险与回滚**:协议只增不减(Clause-② 加宽,Metadata-protocol: added,`@objectstack/spec` minor),新字段全部可选,旧客户端不受影响;每个新部件单独拒绝,不会连累整页。回滚就是还原本 PR。动态部件一次取 200 条,这是已命名的偏差,见下文。 - **先看**:`packages/spec/src/api/record-page.zod.ts`(新成员和流表说明)、`packages/rest/src/record-page-routes.ts`(每个部件就是它替代的那次读)、`ui/packages/app-shell/src/views/RecordDetailView.tsx`(何时请求各部件);文档 `content/docs/frontend/plugins/plugin-detail.mdx` 的 Console Integration 一节改为讲记录页请求。 - **两分钟验证**:`cd ui && pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx`,五条通过:一次 `/page`,零次评论、动态、附件、提及候选和 `?top=1` 读取,含两条消融。 - **需要你做的**:总监做合约审查(CONTRACT_REVIEW_TIER)。声明行已按席位修订的认领改为 `Metadata-protocol: added`,`@objectstack/spec` 定为 minor。PR 保持草稿。 ## What changes One record open is one request. The parts a record page still read on its own after the page payload answered now ride in `POST /api/v1/data/{object}/{id}/page`, and the console takes them from there with no fallback read. ### Contract members added (one complete list for the contract review) `@objectstack/spec` (`packages/spec/src/api/record-page.zod.ts`, exported from `@objectstack/spec/api`): Request (`RecordPageRequestSchema`, still `.strict()`): - `related[].expand?: string[]`: the lookups of a related list's rows to resolve, as that list read's own `$expand`. - `discussion?: boolean`: ask for the record's comment thread and its reactions. - `activity?: { top: int 1..200 }` (`RecordPageActivityRequestSchema`, strict): ask for the newest activity rows and their count. - `attachments?: { top: int 0..100 }` (`RecordPageAttachmentsRequestSchema`, strict): ask for the newest attachments and their count. Response (`RecordPageResponseSchema`): - `discussion?: { comments, reactions } | { error }` (`RecordPageDiscussionSchema` | `RecordPageStreamRefusalSchema`). `reactions` is `{ records } | { error }` (`RecordPageDiscussionReactionsSchema`), so a refused reaction read leaves the comments answered. - `activity?: { total, records } | { error }` (`RecordPageStreamRowsSchema` | `RecordPageStreamRefusalSchema`). - `attachments?: { total, records } | { error }` (same pair). New exports: `RecordPageActivityRequestSchema`, `RecordPageAttachmentsRequestSchema`, `RecordPageStreamRefusalSchema`, `RecordPageStreamRowsSchema`, `RecordPageDiscussionReactionsSchema`, `RecordPageDiscussionSchema`, `RECORD_PAGE_MAX_ACTIVITY_ROWS` (200), `RECORD_PAGE_MAX_ATTACHMENT_ROWS` (100), and for each schema its `X` (input) and `XParsed` (output) type. `@object-ui/types` (`ui/packages/types/src/data.ts`, published): - `RecordPagePayload.discussion?: { comments: Record<string, any>[]; reactions: { records: Record<string, any>[] } | { error: RecordPagePartRefusal } } | { error: RecordPagePartRefusal }` - `RecordPagePayload.activity?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }` - `RecordPagePayload.attachments?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }` - The request type is the spec's `RecordPageRequest`, re-exported, so it gains the request members above with no separate declaration. `@object-ui/react`: `DiscussionContextValue.onMentionSuggestionsWanted?: () => void`. `@object-ui/plugin-detail`: `RichTextCommentInput`, `RecordActivityTimeline` and `RecordChatterPanel` take `onMentionSuggestionsWanted?: () => void`, called on the editor's focus. `record:activity` and `record:chatter` pass the context's hook. ### What each new part answers Each part is the read it replaces, asked as the same caller, behind the stream object's own API exposure gate, and refused on its own: | Part | The read it replaces | | --- | --- | | `related[].expand` | the related list read's own `$expand` (`GET /data/{child}?$expand=…`), spelled as the QueryAST relation map that read's normalizer lowers it to | | `discussion.comments` | `GET /data/sys_comment` where `thread_id` = `{object}:{id}`, oldest first, no page size | | `discussion.reactions` | `GET /data/sys_comment_reaction` where `comment_id` in those comments, oldest first; not asked when the thread is empty | | `activity` | `GET /data/sys_activity` for the record, newest first, `$top`, `$count`, `$expand=actor_id` | | `attachments` | `GET /data/sys_attachment` for the record, newest first, `$top`, `$count` | A record the caller cannot read still refuses the whole request, its thread with it. The body stays strict and the query-parameter set stays empty. ### Console - The page asks `discussion` and `activity` when a block that draws the feed is on screen at open (the 动态 panel of a laid-out page, or a feed block on the tab the page opens on), and `attachments` when the page draws them. The feed, the Attachments panel and the Attachments tab badge draw those parts. A refused part draws the refusal the panel draws today. - The stream asks ride whichever payload the plan asks and never key it, so an assigned page landing after a synthesized one asks nothing again (objectui#11699 stays green). - Each related list asks the lookups of its rows to resolve (every declared reference of the child but the parent's, gated by field-level security), so the rows carry their names. - `RelatedList`'s name fallback no longer resolves the parent record's own id, which no column draws. - The mention roster (`sys_user`, 50 rows) is read on the composer's first focus. A reply or an edit whose text holds an `@` before then reads it when written, so its mentions still resolve. ### Docs - `content/docs/frontend/plugins/plugin-detail.mdx`, the "Console Integration" section only (added to the file surface by claim amendment). It showed the console reading a record's comments with `dataSource.find('sys_comment', …)`, with field names already stale on `main`. It now says a record open takes the discussion and the activity from the `/page` payload, and names when the console still reads `sys_comment` itself (a feed the open did not ask for, or a data source without `getRecordPage`) and when it reads the mention list. Its snippet calls `getRecordPage` with the real types. The rest of the page is unchanged. ### The `?top=1` probe caller `RelatedList`'s reference-name fallback (`ui/packages/plugin-detail/src/RelatedList.tsx`) resolving the payload's bare ids in related rows, one `GET /data/{object}?top=1&filter=["id","in",[…]]` per referenced object, the parent FK itself among them. The rows now arrive expanded, and the parent FK is skipped. ### Named deviation: the activity part asks 200 rows The 动态 panel shows 3 or 5 rows, but its count chip and the 动态 tab count the whole merged stream (comments + activity + field changes, minus dropped types). A 5-row window would make that count wrong, so the part asks the same 200-row window the console's own read asked before. Shrinking it needs a server-side count of the merged stream: see `out_of_scope_findings`. Follow-up: #28737 serves the merged 动态 count with the page payload, so the activity part can ask the panel's few rows. It owns `RECORD_PAGE_MAX_ACTIVITY_ROWS`. ### `/page` once per open (card item 4) Measured on a production build before this change: `/page` goes out once per record. The two other `/page` posts in a drawer open are the previous/next prefetch (#27056, #27545), different ids, by design. So item 4 changes nothing, and the seat recorded it out of scope. ### App-level reads a record open causes (out of scope, left to #28085 / #27626) - In a drawer open: `GET /api/v1/meta/object` (the object list read again on open) and `PATCH /api/v1/data/sys_user_preference/{id}` (the recent-items write). Neither is the record's, and neither is changed here. - On the record route, the other 26 requests are the shell's own boot (runtime config, session, discovery, translations, organizations, auth config, the preference read, `meta/app`, `meta/view`, `meta/page`, `meta/object`, `meta/dashboard`, the app bundle, packages, permissions, localization, settings, AI agents, the approvals inbox, `sys_inbox_message`, `sys_user/{me}` and `meta/object/sys_user`) plus the same preference write. A record open does not cause them. ## Before / after Measured on `examples/app-crm`, production-built Console (`pnpm build && pnpm console:build`, `pnpm dev:crm -- --fresh --ui --no-watch`), Playwright, signed in as admin, every `/api/` request from the click (drawer) or navigation (record route) until network idle plus 3 s. HotCRM was not available, so app-crm stands in. **Contact drawer** (open "Ada Lovelace" from the Contacts list): 13 requests before, 4 after. ``` before after 1. GET /meta/object 1. GET /meta/object 2. POST /data/crm_contact/{ada}/page 2. POST /data/crm_contact/{ada}/page 3. GET /data/sys_user?top=50&select=id,name,email,image (read on the composer's first focus) 4. GET /data/sys_comment?sort=created_at&filter=thread_id = crm_contact:{ada} (in /page: discussion) 5. GET /data/sys_activity?populate=actor_id&top=200&sort=timestamp&… (in /page: activity) 6. POST /data/crm_contact/{prev}/page neighbour prefetch 3. POST /data/crm_contact/{other}/page neighbour prefetch 7. POST /data/crm_contact/{next}/page neighbour prefetch 8. GET /data/sys_organization?top=1&filter=id in [...] (related rows arrive expanded) 9. GET /data/sys_user?top=1&filter=id in [...] (related rows arrive expanded) 10. GET /data/crm_contact?top=1&filter=id in [{ada}] (the parent FK: no longer resolved) 11. GET /data/crm_account?top=1&filter=id in [...] (related rows arrive expanded) 12. GET /data/crm_opportunity?top=1&filter=id in [...] (related rows arrive expanded) 13. PATCH /data/sys_user_preference/{id} 4. PATCH /data/sys_user_preference/{id} ``` The after run has one neighbour prefetch instead of two only because the fresh seed listed Ada third of three (no next row); the prefetch is unchanged. **Account drawer** (open "Acme Corp"): 13 requests before, 5 after: `GET /meta/object`, `POST /data/crm_account/{acme}/page`, the two neighbour prefetches, `PATCH /data/sys_user_preference/{id}`. Gone: `sys_user?top=50`, `sys_comment`, `sys_activity`, and the five `?top=1` probes (`sys_organization`, `sys_user`, `crm_contact`, `crm_account`, `crm_opportunity`). The page request body: ```json {"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"],"related":[{"object":"crm_activity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","contact","opportunity"]},{"object":"crm_contact","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"]},{"object":"crm_lead","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","converted_opportunity"]},{"object":"crm_opportunity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","renewal_of"]}],"explain":["update","delete","transfer"],"fieldAccess":true,"approvals":true,"discussion":true,"activity":{"top":200}} ``` **Contact record route** (navigate to `/apps/crm_app/crm_contact/record/{ada}`, shell boot included): 37 requests before, 28 after. The nine gone: `sys_user?top=50`, `sys_comment`, `sys_activity`, the five `?top=1` probes, and `GET /meta/object/sys_organization` (the schema read the name fallback made for the `sys_organization` probe). Nothing was added. **Composer focus** (account drawer, then a click into "Write a note… @ to mention"): one `GET /data/sys_user?top=50&select=id,name,email,image`, at the focus, and none before it. App-crm does not enable files on these objects, so `attachments` is not asked here; the console test and the dogfood suite cover that part. ## Tests - `packages/spec/src/api/record-page.test.ts`: request strictness for the new members (unknown key in `activity` / `attachments` refused, bounds), and each stream part is its rows or its refusal, never both and never neither. - `packages/rest/src/record-page-routes.test.ts` (7 new, 20 total): each part's read as replaced, for the same caller; an empty thread asks no reaction read; per-part refusal (a refused activity read leaves the others answered); a refused comment read; a refused reaction read keeps the comments; the stream object's exposure gate; strict body. - `packages/qa/dogfood/test/record-page-streams.dogfood.test.ts` (new, real HTTP + security stack, non-admin member): discussion, reactions and activity equal the member's own `GET` reads byte for byte, including a `top: 1` window with the whole count; a private record refuses the whole request as `GET` does; attachments count and list only the record's own (3), never another record's (2 secret). - `packages/qa/dogfood/test/record-page.dogfood.test.ts`: a related list's `expand` is its read's own `$expand`. - `ui/packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx` (new, real `ObjectStackAdapter`, StrictMode): one `/page`; zero `sys_comment`, `sys_comment_reaction`, `sys_activity`, `sys_attachment`, `sys_user?top=50` and `?top=1` reads; the body asks `discussion`, `activity` 200, `attachments` 100 and the related `expand`; the feed, the attachment and the badge draw the payload; the mention read goes out once, on the composer's focus; a refused activity draws `data-denied-source="activity"` and is not read again. Ablations: with no `getRecordPage` the detectors see the `sys_activity`, `sys_attachment` and `sys_comment` reads; with the related `expand` ignored they see the `?top=1` name probe. Removing the parent-FK skip was checked to go red, then restored. - Updated to the payload behavior: `RecordDetailView.recordPagePayload-27627`, `RecordDetailView.recordOpenWaves-13010`, `RecordDetailView.withheldMentions-28218` (the composer reads the roster on focus; a reply reads it at the write). - `ui/apps/console/src/services/approvalRequestsDataSource.servedPage.test.tsx`: it pinned the related timeline's label read of its parent `request_id`, which the related `expand` retires. It now asserts no read of the parent request by id, and still that no read the page makes is refused. ## Acceptance notes - Contract tests: each new part answers the read it replaces for the same caller (route unit tests + dogfood as a member); a refused part is refused on its own; the body is strict. - Console test: one `/page`, zero `sys_comment`, `sys_activity`, `sys_attachment`, `sys_user?top=50` and `?top=1` reads, with ablations. - Before/after request lists: above. - Preview check is the epic seat's. ## Deviations - **The activity part asks 200 rows**, as named above. - **The discussion part is the whole thread, with no page size.** The card asked for the discussion's first page. The part answers the read it replaces: on `main` the console read the whole thread (`sys_comment` where `thread_id` is `{object}:{id}`, oldest first, no `$top`) and held all of it, and the comment thread has no paging door today. A page size needs that door first, which is outside this card. ## out_of_scope_findings - **class b, reach: every record page with a 动态 panel or tab** — a server-side count of the merged 动态 stream (comments + activity + field changes, minus the types the panel drops), so the activity part can ask the panel's 3 or 5 rows instead of 200 and the count chip still holds. Today the count is the client's, over the rows it holds, which is why the part asks 200. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 108 commands, all exit 0 without a PR body (`check-plugin-teardown-shape --self-test` after fetching its pinned fixture commit on this shallow clone; `pnpm --dir ui exec vitest run scripts/` rerun alone after two 15 s timeouts under a concurrent spec suite: 169 files passed). - `@objectstack/spec`: `test` 782 files / 23126 tests passed; `test:repo` 87 files / 1448 passed; `check:generated` current after the merge of main. - `@objectstack/rest`: `typecheck` (incl. test typecheck) 0; `record-page-routes.test.ts` 20 passed. - `packages/qa/dogfood`: `typecheck` 0; the two record page suites 8 passed against the rebuilt rest dist. - ui: 330 test files that import the touched modules, all passed (`--maxWorkers=2` under the verify lock, the record-open files rerun after their updates); `type-check` 0 for `@object-ui/types`, `@object-ui/react`, `@object-ui/plugin-detail`, `@object-ui/app-shell`; eslint 0 errors on every touched file (root config for the platform files); `node scripts/check-sdui-manifest.mjs` 0. - Merged `origin/main` (no text conflicts; generated spec artifacts regenerated, `check-regen-pending` clear). At the merged head: the five record-open ui files (42), the route suite (20) and `record-page.test.ts` (7) rerun green. - Declaration lines per the amended claim (`Metadata-protocol: added`, `@objectstack/spec` at `minor` in `.changeset/28470-record-page-streams.md`, head `579bcda1`). PR-body gates with this body as `GITHUB_EVENT_PATH`: `check-changeset-no-major --base origin/main` 0 (level axis: widening accounted for at `minor`), `check-adr-0087-registration` 0, `check-closing-keyword-parity` 0, `check-empty-changeset` 0, `check-partof-closing-keyword` 0; `pnpm check:changeset-fixed` 0. - Two CI reds at `579bcda1`, fixed at `2369096b`. CI's Test Core 8/8 runs rest's `test:repo`, whose query-slot census (`rest-server-canonical-query-ast.test.ts`) counts every `query:` token in a REST registrar: the stream read's parameter was spelled `query`, so it is renamed and `record-page-routes.ts` is pinned at 2 slots (total 5 → 6). CI's Test UI 9/16 ran the console served-page test above. At `2369096b`: rest `test` and `test:repo` green, the 26 console test files that touch the record page green. - CI at `2369096b`: 65 check runs, 58 success, 7 skipped, none failed; Check Changeset green. - Contract review PASS at `f2e04ba8` (no must-fix). Its should-fixes: `@objectstack/rest` is graded `minor` in `.changeset/28470-record-page-streams.md` at `3b6fec52`, as AGENTS.md's Post-Task Checklist step 3 reads ("a kind takes at least `minor` on a package the diff moves") and as the door's first changeset graded it (spec and rest share one fixed group, so the released version is unchanged; `check-changeset-no-major --base origin/main`, `check:changeset-fixed`, `check-empty-changeset` 0); the whole-thread discussion is named under Deviations; the 200-row cap's follow-up card is #28737. - Docs fix at `f2e04ba8`: the section's `tsx` snippet type-checks against `@object-ui/types` (`tsc` on the extracted fence, 0; a wrong `activity.top` type in it goes red, TS2322). The docs gates the derived list names, all exit 0: `check-docs-section-name` (and `--self-test`), `check-affected-docs`, `check-drift-comment`, `check-doc-route-spelling --advisory`, `check-doc-authoring`, `check:docs-audit-scope`, `check:docs-redirects`, `check:docs-single-h1`, `check:docs-spec-enumerations`, `check:docs-transcript-drift`, `check:issue-citations`, `check:role-word`, `check:nul-bytes`, `check:org-identifier`. Clause-②: yes (widening) Metadata-protocol: added Skills-docs: none — no file under `skills/**` teaches the record page payload (`POST /data/{object}/{id}/page`), its request or response members, or when the console reads the mention roster (a `git grep` of `skills/` for `RecordPage`, `getRecordPage`, `sys_comment`, `sys_attachment` and `mentionSuggestions` names nothing). `skills/objectstack-data` teaches `enable.activities: false` hiding the timeline and the detail-page related lists by relationship flags; both stay true. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [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: its generated artifacts are regenerated and committed. - [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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a314318 commit e7a6cc7

34 files changed

Lines changed: 1860 additions & 169 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/types': patch
3+
'@object-ui/react': patch
4+
'@object-ui/plugin-detail': patch
5+
'@object-ui/app-shell': patch
6+
---
7+
8+
feat(console): a record open is one request — the discussion, the activity, the attachments and the related rows' names come with the page payload (#28470)
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
After the record page payload answered, a record open still sent the comment thread, the activity feed, the attachment list, the mention list and one `?top=1` name read per object the related rows point at. They are gone from the open:
14+
15+
- The page asks the payload for the discussion and the activity when a block that draws the feed is on screen at open (the 动态 panel of a laid-out page, or a feed block on the tab the page opens on), and for the attachments when the page draws them. The feed, the Attachments panel and the Attachments tab badge draw those parts; a refused part draws the refusal the panel draws today. There is no fallback read: a part the payload carries is never read again on the open.
16+
- Each related list asks the lookups of its rows to resolve, as its own read expands them, so the rows carry their names. The related list's name fallback no longer resolves the parent record's own id, which no column draws.
17+
- The mention list (`sys_user`, 50 rows) is read on the comment composer's first focus, not when the record opens. A reply or an edit whose text holds an `@` before then reads it when written, so its mentions still resolve.
18+
19+
- `@object-ui/types`: `RecordPagePayload` gains `discussion`, `activity` and `attachments`.
20+
- `@object-ui/react`: `DiscussionContextValue.onMentionSuggestionsWanted`.
21+
- `@object-ui/plugin-detail`: `RichTextCommentInput`, `RecordActivityTimeline` and `RecordChatterPanel` take `onMentionSuggestionsWanted`, called on the editor's focus; `record:activity` and `record:chatter` pass the context's hook.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/rest': minor
4+
---
5+
6+
feat(rest): the record page payload carries the record's discussion, activity and attachments, and the related rows' names (#28470)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
`POST /api/v1/data/{object}/{id}/page` answers the reads a record page still made on its own after the payload arrived. Each new part is the read it replaces, asked as the same caller behind that object's own exposure gate, and each is refused on its own:
12+
13+
- **`related[].expand`** (request): the lookups of a related list's rows to resolve, as that list read's own `$expand`. The rows carry the referenced records the caller may read in place of their ids, so no name lookup follows the payload.
14+
- **`discussion: true`** (request) → **`discussion`** (response): `{ comments, reactions }` — the record's comment thread (`sys_comment` where `thread_id` is `{object}:{id}`, oldest first, no page size) and those comments' `sys_comment_reaction` rows (oldest first). The reactions carry their own refusal, which leaves the comments answered.
15+
- **`activity: { top }`** (request, 1–200) → **`activity`** (response): `{ total, records }` — the record's newest `sys_activity` rows with their count, `actor_id` expanded.
16+
- **`attachments: { top }`** (request, 0–100) → **`attachments`** (response): `{ total, records }` — the record's newest `sys_attachment` rows with their count.
17+
18+
A stream part is its rows or its refusal (`RecordPageStreamRefusalSchema`), never both and never neither. The body stays strict, the query-parameter set stays empty, and a record the caller cannot read still refuses the whole request. New exports in `@objectstack/spec/api`: `RecordPageActivityRequestSchema`, `RecordPageAttachmentsRequestSchema`, `RecordPageStreamRowsSchema`, `RecordPageStreamRefusalSchema`, `RecordPageDiscussionSchema`, `RecordPageDiscussionReactionsSchema`, `RECORD_PAGE_MAX_ACTIVITY_ROWS`, `RECORD_PAGE_MAX_ATTACHMENT_ROWS`, and their types.

‎content/docs/frontend/plugins/plugin-detail.mdx‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ declare const items: FeedItem[];
100100

101101
### Console Integration
102102

103-
In the Console app, `RecordChatterPanel` is integrated with record detail views. Comments are fetched from the `sys_comment` object using a thread ID pattern of `{objectName}:{recordId}`:
103+
In the Console app, `RecordChatterPanel` is integrated with record detail views. A record
104+
page that draws its feed when it opens takes the discussion from the one request it makes
105+
on open, `POST /api/v1/data/{object}/{id}/page` (the [record page payload](/docs/references/api/record-page)).
106+
That request asks `discussion` (the record's comment thread and those comments' reactions)
107+
and `activity` (its newest activity rows) beside the record, and the page reads neither
108+
`sys_comment` nor `sys_activity` on its own:
104109

105110
```tsx
106111
import type { DataSource, FeedItem } from '@object-ui/types';
@@ -110,26 +115,33 @@ declare const objectName: string;
110115
declare const recordId: string;
111116
declare const dataSource: DataSource;
112117

113-
// Thread ID format
114-
const threadId = `${objectName}:${recordId}`;
115-
116-
// Fetch comments — `find` resolves to a `QueryResult`, so the rows are `.data`.
117-
const comments = await dataSource.find('sys_comment', {
118-
$filter: { threadId },
119-
$orderby: 'createdAt asc',
118+
// The record page's one request; the discussion and the activity are two of its parts.
119+
const page = await dataSource.getRecordPage?.(objectName, recordId, {
120+
discussion: true,
121+
activity: { top: 200 },
120122
});
121123

122-
// Map to FeedItem[]
123-
const feedItems: FeedItem[] = comments.data.map((c) => ({
124-
id: c._id,
124+
// A part is its rows, or the refusal of the read it replaces.
125+
const discussion = page?.discussion;
126+
const comments = discussion && 'comments' in discussion ? discussion.comments : [];
127+
128+
// Map to FeedItem[]: `sys_comment` columns are snake_case.
129+
const feedItems: FeedItem[] = comments.map((c) => ({
130+
id: String(c.id),
125131
type: 'comment',
126-
actor: c.author,
132+
actor: c.author_name,
127133
body: c.body,
128-
createdAt: c.createdAt,
129-
reactions: c.reactions ?? [],
134+
createdAt: c.created_at,
130135
}));
131136
```
132137

138+
A refused part is drawn as refused and is not asked again, and the other parts stay
139+
answered: a refused reaction read leaves the comments shown without reactions. Only a feed
140+
the open did not ask for is read on its own, from `sys_comment` by its `thread_id`
141+
(`{objectName}:{recordId}`), oldest first: for example a feed on a tab the page did not
142+
open on, or any feed on a data source without `getRecordPage`. The composer's mention list
143+
(`sys_user`, 50 rows) is read when the composer is first focused, not when the record opens.
144+
133145
**Full-page detail view** renders the panel non-collapsible at the bottom:
134146

135147
```tsx

0 commit comments

Comments
 (0)