Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/28470-console-record-open-one-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@object-ui/types': patch
'@object-ui/react': patch
'@object-ui/plugin-detail': patch
'@object-ui/app-shell': patch
---

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)

Clause-②: yes (widening)
Metadata-protocol: none

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:

- 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.
- 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.
- 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.

- `@object-ui/types`: `RecordPagePayload` gains `discussion`, `activity` and `attachments`.
- `@object-ui/react`: `DiscussionContextValue.onMentionSuggestionsWanted`.
- `@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.
18 changes: 18 additions & 0 deletions .changeset/28470-record-page-streams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/spec': minor
'@objectstack/rest': minor
---

feat(rest): the record page payload carries the record's discussion, activity and attachments, and the related rows' names (#28470)

Clause-②: yes (widening)
Metadata-protocol: added

`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:

- **`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.
- **`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.
- **`activity: { top }`** (request, 1–200) → **`activity`** (response): `{ total, records }` — the record's newest `sys_activity` rows with their count, `actor_id` expanded.
- **`attachments: { top }`** (request, 0–100) → **`attachments`** (response): `{ total, records }` — the record's newest `sys_attachment` rows with their count.

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.
40 changes: 26 additions & 14 deletions content/docs/frontend/plugins/plugin-detail.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ declare const items: FeedItem[];

### Console Integration

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}`:
In the Console app, `RecordChatterPanel` is integrated with record detail views. A record
page that draws its feed when it opens takes the discussion from the one request it makes
on open, `POST /api/v1/data/{object}/{id}/page` (the [record page payload](/docs/references/api/record-page)).
That request asks `discussion` (the record's comment thread and those comments' reactions)
and `activity` (its newest activity rows) beside the record, and the page reads neither
`sys_comment` nor `sys_activity` on its own:

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

// Thread ID format
const threadId = `${objectName}:${recordId}`;

// Fetch comments — `find` resolves to a `QueryResult`, so the rows are `.data`.
const comments = await dataSource.find('sys_comment', {
$filter: { threadId },
$orderby: 'createdAt asc',
// The record page's one request; the discussion and the activity are two of its parts.
const page = await dataSource.getRecordPage?.(objectName, recordId, {
discussion: true,
activity: { top: 200 },
});

// Map to FeedItem[]
const feedItems: FeedItem[] = comments.data.map((c) => ({
id: c._id,
// A part is its rows, or the refusal of the read it replaces.
const discussion = page?.discussion;
const comments = discussion && 'comments' in discussion ? discussion.comments : [];

// Map to FeedItem[]: `sys_comment` columns are snake_case.
const feedItems: FeedItem[] = comments.map((c) => ({
id: String(c.id),
type: 'comment',
actor: c.author,
actor: c.author_name,
body: c.body,
createdAt: c.createdAt,
reactions: c.reactions ?? [],
createdAt: c.created_at,
}));
```

A refused part is drawn as refused and is not asked again, and the other parts stay
answered: a refused reaction read leaves the comments shown without reactions. Only a feed
the open did not ask for is read on its own, from `sys_comment` by its `thread_id`
(`{objectName}:{recordId}`), oldest first: for example a feed on a tab the page did not
open on, or any feed on a data source without `getRecordPage`. The composer's mention list
(`sys_user`, 50 rows) is read when the composer is first focused, not when the record opens.

**Full-page detail view** renders the panel non-collapsible at the bottom:

```tsx
Expand Down
Loading
Loading