Skip to content

Commit e236764

Browse files
os-zhuangclaude
andauthored
feat(detail): timeline drill-to-source link (ADR-0052 ActivityPointer) (#1779)
Pairs with framework's sys_activity.source_object/source_id (ADR-0052 §5). When an activity row carries a source pointer — an email derived from a sys_email row, a call/meeting task — the record timeline now renders a "View source →" link so the user can drill from the one-line summary to the full record. Reuses the URL pattern already used by InboxPopover for notification source pointers. - types: FeedItem gains sourceObject/sourceId (distinct from `source` = origin channel) - app-shell RecordDetailView: map sys_activity.source_object/source_id → FeedItem - plugin-detail RecordActivityTimeline: render the drill link when present - i18n: detail.viewSource - test: link renders with correct href when source present; absent otherwise (2/2) Reactions + threading were already wired (sys_comment.reactions/parent_id in RecordDetailView); this adds the missing ActivityPointer drill-through. NOTE: app-shell has a pre-existing typecheck error on `actionDescription` (RecordDetailView:122, useConsoleActionRuntime:90) unrelated to this change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3e59c60 commit e236764

5 files changed

Lines changed: 71 additions & 0 deletions

File tree

packages/app-shell/src/views/RecordDetailView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
991991
actorAvatarUrl: row.actor_avatar_url ?? undefined,
992992
body: row.summary ?? '',
993993
createdAt: when,
994+
// ADR-0052 ActivityPointer: drill from the summary to the source
995+
// rich entity (sys_email row, call/meeting task, …) when present.
996+
sourceObject: row.source_object ?? undefined,
997+
sourceId: row.source_id ?? undefined,
994998
} as FeedItem);
995999
}
9961000
if (!mapped.length) return;

packages/plugin-detail/src/RecordActivityTimeline.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,18 @@ export const RecordActivityTimeline: React.FC<RecordActivityTimelineProps> = ({
414414
</p>
415415
)}
416416

417+
{/* Drill to source rich entity (ADR-0052 ActivityPointer) */}
418+
{item.sourceObject && item.sourceId != null && (
419+
<a
420+
href={`/objects/${item.sourceObject}/${item.sourceId}`}
421+
className="mt-1 inline-flex items-center gap-1 text-xs text-primary hover:underline"
422+
data-testid="activity-source-link"
423+
>
424+
{t('detail.viewSource')}
425+
<span aria-hidden></span>
426+
</a>
427+
)}
428+
417429
{/* Field changes */}
418430
{item.type === 'field_change' && item.fieldChanges && (
419431
<div className="space-y-1 mt-1">
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* ADR-0052 ActivityPointer: a timeline row carrying a `sourceObject`/`sourceId`
6+
* pointer (e.g. an email derived from a `sys_email` row) renders a "view source"
7+
* link so the user can drill from the one-line summary to the full record.
8+
*/
9+
import { describe, it, expect } from 'vitest';
10+
import { render, screen } from '@testing-library/react';
11+
import { RecordActivityTimeline } from '../RecordActivityTimeline';
12+
import type { FeedItem } from '@object-ui/types';
13+
14+
describe('RecordActivityTimeline — source drill link (ADR-0052 ActivityPointer)', () => {
15+
it('renders a "view source" link to the source entity when sourceObject/sourceId are present', () => {
16+
const items: FeedItem[] = [
17+
{
18+
id: 'act-1',
19+
type: 'comment',
20+
actor: 'System',
21+
body: 'Email: Q3 proposal',
22+
createdAt: new Date().toISOString(),
23+
sourceObject: 'sys_email',
24+
sourceId: 'email-123',
25+
},
26+
];
27+
render(<RecordActivityTimeline items={items} />);
28+
const link = screen.getByTestId('activity-source-link') as HTMLAnchorElement;
29+
expect(link).toBeTruthy();
30+
expect(link.getAttribute('href')).toBe('/objects/sys_email/email-123');
31+
});
32+
33+
it('renders no source link when the activity has no source pointer', () => {
34+
const items: FeedItem[] = [
35+
{
36+
id: 'act-2',
37+
type: 'comment',
38+
actor: 'System',
39+
body: 'Status: To Do → Done',
40+
createdAt: new Date().toISOString(),
41+
},
42+
];
43+
render(<RecordActivityTimeline items={items} />);
44+
expect(screen.queryByTestId('activity-source-link')).toBeNull();
45+
});
46+
});

packages/plugin-detail/src/useDetailTranslation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const DETAIL_DEFAULT_TRANSLATIONS: Record<string, string> = {
173173
'detail.loadMore': 'Load more',
174174
'detail.edited': '(edited)',
175175
'detail.via': 'via {{source}}',
176+
'detail.viewSource': 'View source',
176177
// Replies
177178
'detail.replyCount': '{{count}} reply',
178179
'detail.replyCountPlural': '{{count}} replies',

packages/types/src/views.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,14 @@ export interface FeedItem {
360360
pinned?: boolean;
361361
/** Whether this item has been edited */
362362
edited?: boolean;
363+
/**
364+
* Source rich-entity pointer (ADR-0052 ActivityPointer). When the activity
365+
* was derived from a separate record — an email in `sys_email`, a call/meeting
366+
* task — these identify it so the timeline can drill from the one-line summary
367+
* to the full record. Distinct from `source` (the origin channel).
368+
*/
369+
sourceObject?: string;
370+
sourceId?: string | number;
363371
}
364372

365373
/**

0 commit comments

Comments
 (0)