@feedclip/activity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/ActivityPanel.d.ts +68 -0
- package/dist/ActivityPanel.d.ts.map +1 -0
- package/dist/activity.d.ts +118 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/adapters/memory.cjs +152 -0
- package/dist/adapters/memory.cjs.map +7 -0
- package/dist/adapters/memory.d.ts +2 -0
- package/dist/adapters/memory.d.ts.map +1 -0
- package/dist/adapters/memory.js +131 -0
- package/dist/adapters/memory.js.map +7 -0
- package/dist/adapters/postgres.cjs +287 -0
- package/dist/adapters/postgres.cjs.map +7 -0
- package/dist/adapters/postgres.d.ts +2 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +266 -0
- package/dist/adapters/postgres.js.map +7 -0
- package/dist/index.cjs +249 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +7 -0
- package/dist/react.cjs +476 -0
- package/dist/react.cjs.map +7 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +455 -0
- package/dist/react.js.map +7 -0
- package/dist/styles.css +1 -0
- package/migrations/001_activity_schema.sql +44 -0
- package/package.json +86 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/ActivityPanel.tsx
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
|
|
8
|
+
// src/activity.ts
|
|
9
|
+
var ActivityError = class extends Error {
|
|
10
|
+
constructor(code, message, field) {
|
|
11
|
+
super(message);
|
|
12
|
+
__publicField(this, "code");
|
|
13
|
+
__publicField(this, "field");
|
|
14
|
+
this.name = "ActivityError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.field = field;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/ActivityPanel.tsx
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
var filterActions = {
|
|
23
|
+
updates: ["update"],
|
|
24
|
+
content: ["comment", "attachment"],
|
|
25
|
+
lifecycle: ["create", "delete", "archive", "restore"]
|
|
26
|
+
};
|
|
27
|
+
var defaultMessages = {
|
|
28
|
+
panelLabel: "Activity history",
|
|
29
|
+
title: "Activity",
|
|
30
|
+
developer: "Developer",
|
|
31
|
+
searchLabel: "Search activity",
|
|
32
|
+
searchPlaceholder: "Search...",
|
|
33
|
+
filterLabel: "Filter by action family",
|
|
34
|
+
filterAll: "All",
|
|
35
|
+
filterUpdates: "Updates",
|
|
36
|
+
filterContent: "Content",
|
|
37
|
+
filterLifecycle: "Lifecycle",
|
|
38
|
+
loadingLabel: "Loading activity",
|
|
39
|
+
entryActionsLabel: "Entry actions",
|
|
40
|
+
copyIdLabel: "Copy activity id",
|
|
41
|
+
copyIdTitle: "Copy ID",
|
|
42
|
+
changedBy: "Changed by",
|
|
43
|
+
at: "At",
|
|
44
|
+
source: "Source",
|
|
45
|
+
version: "Version",
|
|
46
|
+
application: "Application",
|
|
47
|
+
unversioned: "Unversioned",
|
|
48
|
+
noActivity: "No activity yet",
|
|
49
|
+
noMatches: "No matching activity",
|
|
50
|
+
noActivityHelp: "Tracked entries for this resource will appear here.",
|
|
51
|
+
noMatchesHelp: "Try a broader search or a different action family.",
|
|
52
|
+
loadError: "Activity could not load",
|
|
53
|
+
loadErrorFallback: "The activity query returned an error.",
|
|
54
|
+
none: "None",
|
|
55
|
+
moreChanges: "more changes",
|
|
56
|
+
updated: "Updated",
|
|
57
|
+
updatedSuffix: "updated",
|
|
58
|
+
comment: "Comment",
|
|
59
|
+
attachment: "Attachment",
|
|
60
|
+
reason: "Reason",
|
|
61
|
+
createdSuffix: "created"
|
|
62
|
+
};
|
|
63
|
+
function ActivityPanel({
|
|
64
|
+
activity,
|
|
65
|
+
resource,
|
|
66
|
+
entries: controlledEntries,
|
|
67
|
+
filters,
|
|
68
|
+
onEntryClick,
|
|
69
|
+
onError,
|
|
70
|
+
onQueryChange,
|
|
71
|
+
search,
|
|
72
|
+
variant = "default",
|
|
73
|
+
messages: messageOverrides,
|
|
74
|
+
locale
|
|
75
|
+
}) {
|
|
76
|
+
const messages = { ...defaultMessages, ...messageOverrides };
|
|
77
|
+
const actionLabels = {
|
|
78
|
+
all: messages.filterAll,
|
|
79
|
+
updates: messages.filterUpdates,
|
|
80
|
+
content: messages.filterContent,
|
|
81
|
+
lifecycle: messages.filterLifecycle
|
|
82
|
+
};
|
|
83
|
+
const [entries, setEntries] = useState(controlledEntries ?? []);
|
|
84
|
+
const [status, setStatus] = useState(
|
|
85
|
+
controlledEntries ? "ready" : "loading"
|
|
86
|
+
);
|
|
87
|
+
const [error, setError] = useState(null);
|
|
88
|
+
const [localSearch, setLocalSearch] = useState("");
|
|
89
|
+
const [filter, setFilter] = useState("all");
|
|
90
|
+
const [developerMode, setDeveloperMode] = useState(false);
|
|
91
|
+
const query = search ?? localSearch;
|
|
92
|
+
const activeActions = useMemo(() => {
|
|
93
|
+
if (filters?.actions?.length) {
|
|
94
|
+
return filters.actions;
|
|
95
|
+
}
|
|
96
|
+
return filter === "all" ? void 0 : filterActions[filter];
|
|
97
|
+
}, [filter, filters?.actions]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (controlledEntries) {
|
|
100
|
+
setEntries(controlledEntries);
|
|
101
|
+
setStatus("ready");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let cancelled = false;
|
|
105
|
+
setStatus("loading");
|
|
106
|
+
setError(null);
|
|
107
|
+
const queryOptions = {
|
|
108
|
+
resource,
|
|
109
|
+
search: query,
|
|
110
|
+
actions: activeActions,
|
|
111
|
+
actor: filters?.actor,
|
|
112
|
+
from: filters?.from,
|
|
113
|
+
to: filters?.to
|
|
114
|
+
};
|
|
115
|
+
onQueryChange?.(queryOptions);
|
|
116
|
+
activity.query(queryOptions).then((nextEntries) => {
|
|
117
|
+
if (!cancelled) {
|
|
118
|
+
setEntries(nextEntries);
|
|
119
|
+
setStatus("ready");
|
|
120
|
+
}
|
|
121
|
+
}).catch((unknownError) => {
|
|
122
|
+
const nextError = unknownError instanceof Error ? unknownError : new ActivityError("UNKNOWN_ERROR", "Activity could not load");
|
|
123
|
+
if (!cancelled) {
|
|
124
|
+
setError(nextError);
|
|
125
|
+
setStatus("error");
|
|
126
|
+
onError?.(nextError);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
cancelled = true;
|
|
131
|
+
};
|
|
132
|
+
}, [
|
|
133
|
+
activity,
|
|
134
|
+
activeActions,
|
|
135
|
+
controlledEntries,
|
|
136
|
+
filters?.actor,
|
|
137
|
+
filters?.from,
|
|
138
|
+
filters?.to,
|
|
139
|
+
onError,
|
|
140
|
+
onQueryChange,
|
|
141
|
+
query,
|
|
142
|
+
resource
|
|
143
|
+
]);
|
|
144
|
+
return /* @__PURE__ */ jsxs(
|
|
145
|
+
"section",
|
|
146
|
+
{
|
|
147
|
+
className: `activity-panel activity-panel--${variant}`,
|
|
148
|
+
"aria-label": messages.panelLabel,
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsxs("header", { className: "activity-panel__header", children: [
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
152
|
+
/* @__PURE__ */ jsx("p", { className: "eyebrow", children: resource.type }),
|
|
153
|
+
/* @__PURE__ */ jsx("h1", { children: messages.title })
|
|
154
|
+
] }),
|
|
155
|
+
/* @__PURE__ */ jsxs("label", { className: "developer-toggle", children: [
|
|
156
|
+
/* @__PURE__ */ jsx(
|
|
157
|
+
"input",
|
|
158
|
+
{
|
|
159
|
+
checked: developerMode,
|
|
160
|
+
onChange: (event) => setDeveloperMode(event.currentTarget.checked),
|
|
161
|
+
type: "checkbox"
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
messages.developer
|
|
165
|
+
] })
|
|
166
|
+
] }),
|
|
167
|
+
/* @__PURE__ */ jsxs("div", { className: "activity-toolbar", children: [
|
|
168
|
+
/* @__PURE__ */ jsxs("label", { className: "search-field", children: [
|
|
169
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" }),
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
"input",
|
|
172
|
+
{
|
|
173
|
+
"aria-label": messages.searchLabel,
|
|
174
|
+
disabled: search !== void 0,
|
|
175
|
+
onChange: (event) => setLocalSearch(event.currentTarget.value),
|
|
176
|
+
placeholder: messages.searchPlaceholder,
|
|
177
|
+
value: query
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
] }),
|
|
181
|
+
/* @__PURE__ */ jsx("div", { className: "segmented-control", "aria-label": messages.filterLabel, children: Object.keys(actionLabels).map((key) => /* @__PURE__ */ jsx(
|
|
182
|
+
"button",
|
|
183
|
+
{
|
|
184
|
+
"aria-pressed": filter === key,
|
|
185
|
+
className: filter === key ? "is-active" : "",
|
|
186
|
+
disabled: Boolean(filters?.actions?.length),
|
|
187
|
+
onClick: () => setFilter(key),
|
|
188
|
+
type: "button",
|
|
189
|
+
children: actionLabels[key]
|
|
190
|
+
},
|
|
191
|
+
key
|
|
192
|
+
)) })
|
|
193
|
+
] }),
|
|
194
|
+
status === "loading" ? /* @__PURE__ */ jsx(ActivityLoading, { messages }) : null,
|
|
195
|
+
status === "error" ? /* @__PURE__ */ jsx(ActivityErrorState, { error, messages }) : null,
|
|
196
|
+
status === "ready" && entries.length === 0 ? /* @__PURE__ */ jsx(ActivityEmpty, { hasQuery: query.trim().length > 0 || filter !== "all", messages }) : null,
|
|
197
|
+
status === "ready" && entries.length > 0 ? /* @__PURE__ */ jsx("div", { className: "activity-stream", children: entries.map((entry) => /* @__PURE__ */ jsx(
|
|
198
|
+
ActivityEntryRow,
|
|
199
|
+
{
|
|
200
|
+
developerMode,
|
|
201
|
+
entry,
|
|
202
|
+
onEntryClick,
|
|
203
|
+
messages,
|
|
204
|
+
locale
|
|
205
|
+
},
|
|
206
|
+
entry.id
|
|
207
|
+
)) }) : null
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
function ActivityEntryRow({
|
|
213
|
+
developerMode,
|
|
214
|
+
entry,
|
|
215
|
+
onEntryClick,
|
|
216
|
+
messages,
|
|
217
|
+
locale
|
|
218
|
+
}) {
|
|
219
|
+
const [expanded, setExpanded] = useState(false);
|
|
220
|
+
const summary = getEntrySummary(entry, messages);
|
|
221
|
+
return /* @__PURE__ */ jsxs("article", { className: "activity-entry", children: [
|
|
222
|
+
/* @__PURE__ */ jsxs(
|
|
223
|
+
"button",
|
|
224
|
+
{
|
|
225
|
+
"aria-expanded": expanded,
|
|
226
|
+
className: "entry-main",
|
|
227
|
+
onClick: () => {
|
|
228
|
+
setExpanded((value) => !value);
|
|
229
|
+
onEntryClick?.(entry);
|
|
230
|
+
},
|
|
231
|
+
onKeyDown: (event) => {
|
|
232
|
+
if (event.key === "Escape") {
|
|
233
|
+
setExpanded(false);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
237
|
+
const stream = event.currentTarget.closest(".activity-stream");
|
|
238
|
+
const toggles = Array.from(
|
|
239
|
+
stream?.querySelectorAll("button.entry-main") ?? []
|
|
240
|
+
);
|
|
241
|
+
const currentIndex = toggles.indexOf(event.currentTarget);
|
|
242
|
+
const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? toggles.length - 1 : event.key === "ArrowDown" ? Math.min(currentIndex + 1, toggles.length - 1) : Math.max(currentIndex - 1, 0);
|
|
243
|
+
if (toggles[nextIndex]) {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
toggles[nextIndex].focus();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
type: "button",
|
|
250
|
+
children: [
|
|
251
|
+
/* @__PURE__ */ jsx("span", { className: `entry-icon entry-icon--${summary.family}`, children: /* @__PURE__ */ jsx("span", { className: `gg-icon gg-icon--${summary.icon}`, "aria-hidden": "true" }) }),
|
|
252
|
+
/* @__PURE__ */ jsxs("span", { className: "entry-body", children: [
|
|
253
|
+
/* @__PURE__ */ jsx("span", { className: "entry-kicker", children: summary.title }),
|
|
254
|
+
/* @__PURE__ */ jsx(EntryContent, { entry, expanded, messages, locale }),
|
|
255
|
+
/* @__PURE__ */ jsxs("span", { className: "entry-meta", children: [
|
|
256
|
+
entry.actor.name,
|
|
257
|
+
" ",
|
|
258
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "-" }),
|
|
259
|
+
" ",
|
|
260
|
+
formatTime(entry.timestamp, locale)
|
|
261
|
+
] })
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsx("span", { className: "expand-indicator", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
264
|
+
"span",
|
|
265
|
+
{
|
|
266
|
+
className: `gg-icon gg-icon--chevron ${expanded ? "is-open" : ""}`
|
|
267
|
+
}
|
|
268
|
+
) })
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
),
|
|
272
|
+
/* @__PURE__ */ jsx("div", { className: "entry-actions", "aria-label": messages.entryActionsLabel, children: /* @__PURE__ */ jsx(
|
|
273
|
+
"button",
|
|
274
|
+
{
|
|
275
|
+
"aria-label": messages.copyIdLabel,
|
|
276
|
+
onClick: () => void copyText(entry.id),
|
|
277
|
+
title: messages.copyIdTitle,
|
|
278
|
+
type: "button",
|
|
279
|
+
children: /* @__PURE__ */ jsx("span", { className: "gg-icon gg-icon--copy", "aria-hidden": "true" })
|
|
280
|
+
}
|
|
281
|
+
) }),
|
|
282
|
+
expanded ? /* @__PURE__ */ jsxs("div", { className: "entry-details", children: [
|
|
283
|
+
/* @__PURE__ */ jsxs("dl", { children: [
|
|
284
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
285
|
+
/* @__PURE__ */ jsx("dt", { children: messages.changedBy }),
|
|
286
|
+
/* @__PURE__ */ jsxs("dd", { children: [
|
|
287
|
+
entry.actor.name,
|
|
288
|
+
" ",
|
|
289
|
+
/* @__PURE__ */ jsx("span", { children: entry.actor.type })
|
|
290
|
+
] })
|
|
291
|
+
] }),
|
|
292
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
293
|
+
/* @__PURE__ */ jsx("dt", { children: messages.at }),
|
|
294
|
+
/* @__PURE__ */ jsx("dd", { children: formatFullDate(entry.timestamp, locale) })
|
|
295
|
+
] }),
|
|
296
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
297
|
+
/* @__PURE__ */ jsx("dt", { children: messages.source }),
|
|
298
|
+
/* @__PURE__ */ jsx("dd", { children: readMetadata(entry, "source") ?? messages.application })
|
|
299
|
+
] }),
|
|
300
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
301
|
+
/* @__PURE__ */ jsx("dt", { children: messages.version }),
|
|
302
|
+
/* @__PURE__ */ jsx("dd", { children: readMetadata(entry, "version") ?? messages.unversioned })
|
|
303
|
+
] })
|
|
304
|
+
] }),
|
|
305
|
+
developerMode ? /* @__PURE__ */ jsx("pre", { className: "developer-json", children: JSON.stringify(serializeEntry(entry), null, 2) }) : null
|
|
306
|
+
] }) : null
|
|
307
|
+
] });
|
|
308
|
+
}
|
|
309
|
+
function EntryContent({
|
|
310
|
+
entry,
|
|
311
|
+
expanded,
|
|
312
|
+
messages,
|
|
313
|
+
locale
|
|
314
|
+
}) {
|
|
315
|
+
if (entry.action === "update") {
|
|
316
|
+
return /* @__PURE__ */ jsx(UpdateChanges, { changes: entry.changes ?? [], expanded, messages, locale });
|
|
317
|
+
}
|
|
318
|
+
if (entry.action === "comment" && entry.content?.type === "comment") {
|
|
319
|
+
return /* @__PURE__ */ jsx("span", { className: "content-preview", children: /* @__PURE__ */ jsx("q", { children: entry.content.text }) });
|
|
320
|
+
}
|
|
321
|
+
if (entry.action === "attachment" && entry.content?.type === "attachment") {
|
|
322
|
+
return /* @__PURE__ */ jsxs("span", { className: "content-preview", children: [
|
|
323
|
+
/* @__PURE__ */ jsx("strong", { children: entry.content.fileName }),
|
|
324
|
+
/* @__PURE__ */ jsxs("small", { children: [
|
|
325
|
+
formatBytes(entry.content.size),
|
|
326
|
+
" - ",
|
|
327
|
+
entry.content.mimeType
|
|
328
|
+
] })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
return /* @__PURE__ */ jsx("span", { className: "content-preview", children: getLifecycleBody(entry, messages) });
|
|
332
|
+
}
|
|
333
|
+
function UpdateChanges({
|
|
334
|
+
changes,
|
|
335
|
+
expanded,
|
|
336
|
+
messages,
|
|
337
|
+
locale
|
|
338
|
+
}) {
|
|
339
|
+
const visibleChanges = expanded ? changes : changes.slice(0, 3);
|
|
340
|
+
const hiddenCount = changes.length - visibleChanges.length;
|
|
341
|
+
return /* @__PURE__ */ jsxs("span", { className: "changes-list", children: [
|
|
342
|
+
visibleChanges.map((change) => /* @__PURE__ */ jsxs("span", { className: "change-row", children: [
|
|
343
|
+
/* @__PURE__ */ jsx("span", { children: change.label }),
|
|
344
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
345
|
+
/* @__PURE__ */ jsx("del", { children: formatValue(change.before, messages, locale) }),
|
|
346
|
+
/* @__PURE__ */ jsx("ins", { children: formatValue(change.after, messages, locale) })
|
|
347
|
+
] })
|
|
348
|
+
] }, `${change.field}-${change.label}`)),
|
|
349
|
+
hiddenCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "more-changes", children: [
|
|
350
|
+
"+",
|
|
351
|
+
hiddenCount,
|
|
352
|
+
" ",
|
|
353
|
+
messages.moreChanges
|
|
354
|
+
] }) : null
|
|
355
|
+
] });
|
|
356
|
+
}
|
|
357
|
+
function ActivityLoading({ messages }) {
|
|
358
|
+
return /* @__PURE__ */ jsx("div", { className: "activity-skeleton", "aria-label": messages.loadingLabel, children: Array.from({ length: 4 }, (_, index) => /* @__PURE__ */ jsxs("div", { className: "skeleton-row", children: [
|
|
359
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
360
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
361
|
+
/* @__PURE__ */ jsx("b", {}),
|
|
362
|
+
/* @__PURE__ */ jsx("i", {})
|
|
363
|
+
] })
|
|
364
|
+
] }, index)) });
|
|
365
|
+
}
|
|
366
|
+
function ActivityEmpty({ hasQuery, messages }) {
|
|
367
|
+
return /* @__PURE__ */ jsxs("div", { className: "empty-state", children: [
|
|
368
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "-" }),
|
|
369
|
+
/* @__PURE__ */ jsx("h2", { children: hasQuery ? messages.noMatches : messages.noActivity }),
|
|
370
|
+
/* @__PURE__ */ jsx("p", { children: hasQuery ? messages.noMatchesHelp : messages.noActivityHelp })
|
|
371
|
+
] });
|
|
372
|
+
}
|
|
373
|
+
function ActivityErrorState({ error, messages }) {
|
|
374
|
+
return /* @__PURE__ */ jsxs("div", { className: "empty-state", children: [
|
|
375
|
+
/* @__PURE__ */ jsx("h2", { children: messages.loadError }),
|
|
376
|
+
/* @__PURE__ */ jsx("p", { children: error?.message ?? messages.loadErrorFallback })
|
|
377
|
+
] });
|
|
378
|
+
}
|
|
379
|
+
function getEntrySummary(entry, messages) {
|
|
380
|
+
if (entry.action === "update") {
|
|
381
|
+
return {
|
|
382
|
+
title: (entry.changes?.length ?? 0) > 1 ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}` : entry.changes?.[0]?.label ?? messages.updated,
|
|
383
|
+
icon: "file-document",
|
|
384
|
+
family: "update"
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (entry.action === "comment") {
|
|
388
|
+
return { title: messages.comment, icon: "comment", family: "content" };
|
|
389
|
+
}
|
|
390
|
+
if (entry.action === "attachment") {
|
|
391
|
+
return { title: messages.attachment, icon: "attachment", family: "content" };
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
title: titleize(entry.action),
|
|
395
|
+
icon: entry.action === "archive" ? "box" : "add",
|
|
396
|
+
family: "lifecycle"
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function getLifecycleBody(entry, messages) {
|
|
400
|
+
if (entry.action === "create") {
|
|
401
|
+
return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;
|
|
402
|
+
}
|
|
403
|
+
const reason = readMetadata(entry, "reason");
|
|
404
|
+
return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);
|
|
405
|
+
}
|
|
406
|
+
function readMetadata(entry, key) {
|
|
407
|
+
const value = entry.metadata?.[key];
|
|
408
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
409
|
+
}
|
|
410
|
+
function formatTime(date, locale) {
|
|
411
|
+
return new Intl.DateTimeFormat(locale, {
|
|
412
|
+
hour: "2-digit",
|
|
413
|
+
minute: "2-digit"
|
|
414
|
+
}).format(date);
|
|
415
|
+
}
|
|
416
|
+
function formatFullDate(date, locale) {
|
|
417
|
+
return new Intl.DateTimeFormat(locale, {
|
|
418
|
+
dateStyle: "medium",
|
|
419
|
+
timeStyle: "short"
|
|
420
|
+
}).format(date);
|
|
421
|
+
}
|
|
422
|
+
function formatBytes(bytes) {
|
|
423
|
+
if (bytes < 1024) {
|
|
424
|
+
return `${bytes} B`;
|
|
425
|
+
}
|
|
426
|
+
const kilobytes = bytes / 1024;
|
|
427
|
+
return `${Math.round(kilobytes)} KB`;
|
|
428
|
+
}
|
|
429
|
+
function formatValue(value, messages, locale) {
|
|
430
|
+
if (value === null || value === void 0) {
|
|
431
|
+
return messages.none;
|
|
432
|
+
}
|
|
433
|
+
if (value instanceof Date) {
|
|
434
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(value);
|
|
435
|
+
}
|
|
436
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : JSON.stringify(value);
|
|
437
|
+
}
|
|
438
|
+
function titleize(value) {
|
|
439
|
+
return value.replace(/[._-]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
440
|
+
}
|
|
441
|
+
function serializeEntry(entry) {
|
|
442
|
+
return {
|
|
443
|
+
...entry,
|
|
444
|
+
timestamp: entry.timestamp.toISOString()
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function copyText(value) {
|
|
448
|
+
if (globalThis.navigator?.clipboard) {
|
|
449
|
+
await globalThis.navigator.clipboard.writeText(value);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export {
|
|
453
|
+
ActivityPanel
|
|
454
|
+
};
|
|
455
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/ActivityPanel.tsx", "../src/activity.ts"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useMemo, useState } from \"react\";\nimport {\n type Action,\n type Activity,\n type ActivityRecord,\n ActivityError,\n type BuiltInAction,\n type Change,\n type ResourceReference,\n} from \"./activity\";\n\nexport type ActivityPanelMessages = typeof defaultMessages;\n\nexport type ActivityPanelProps = {\n activity: Activity;\n resource: ResourceReference;\n entries?: ActivityRecord[];\n search?: string;\n filters?: ActivityFilters;\n variant?: \"default\" | \"compact\" | \"comfortable\";\n onEntryClick?: (entry: ActivityRecord) => void;\n onError?: (error: Error) => void;\n onQueryChange?: (query: ActivityPanelQuery) => void;\n messages?: Partial<ActivityPanelMessages>;\n locale?: string;\n};\n\ntype ActivityFilters = {\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\nexport type ActivityPanelQuery = {\n resource: ResourceReference;\n search?: string;\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\ntype Filter = \"all\" | \"updates\" | \"content\" | \"lifecycle\";\ntype IconName = \"add\" | \"attachment\" | \"box\" | \"chevron-right\" | \"comment\" | \"copy\" | \"file-document\";\ntype EntrySummary = {\n title: string;\n icon: IconName;\n family: \"update\" | \"content\" | \"lifecycle\";\n};\n\nconst filterActions: Record<Exclude<Filter, \"all\">, BuiltInAction[]> = {\n updates: [\"update\"],\n content: [\"comment\", \"attachment\"],\n lifecycle: [\"create\", \"delete\", \"archive\", \"restore\"],\n};\n\nconst defaultMessages = {\n panelLabel: \"Activity history\",\n title: \"Activity\",\n developer: \"Developer\",\n searchLabel: \"Search activity\",\n searchPlaceholder: \"Search...\",\n filterLabel: \"Filter by action family\",\n filterAll: \"All\",\n filterUpdates: \"Updates\",\n filterContent: \"Content\",\n filterLifecycle: \"Lifecycle\",\n loadingLabel: \"Loading activity\",\n entryActionsLabel: \"Entry actions\",\n copyIdLabel: \"Copy activity id\",\n copyIdTitle: \"Copy ID\",\n changedBy: \"Changed by\",\n at: \"At\",\n source: \"Source\",\n version: \"Version\",\n application: \"Application\",\n unversioned: \"Unversioned\",\n noActivity: \"No activity yet\",\n noMatches: \"No matching activity\",\n noActivityHelp: \"Tracked entries for this resource will appear here.\",\n noMatchesHelp: \"Try a broader search or a different action family.\",\n loadError: \"Activity could not load\",\n loadErrorFallback: \"The activity query returned an error.\",\n none: \"None\",\n moreChanges: \"more changes\",\n updated: \"Updated\",\n updatedSuffix: \"updated\",\n comment: \"Comment\",\n attachment: \"Attachment\",\n reason: \"Reason\",\n createdSuffix: \"created\",\n} as const;\n\nexport function ActivityPanel({\n activity,\n resource,\n entries: controlledEntries,\n filters,\n onEntryClick,\n onError,\n onQueryChange,\n search,\n variant = \"default\",\n messages: messageOverrides,\n locale,\n}: ActivityPanelProps) {\n const messages = { ...defaultMessages, ...messageOverrides };\n const actionLabels: Record<Filter, string> = {\n all: messages.filterAll,\n updates: messages.filterUpdates,\n content: messages.filterContent,\n lifecycle: messages.filterLifecycle,\n };\n const [entries, setEntries] = useState<ActivityRecord[]>(controlledEntries ?? []);\n const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\n controlledEntries ? \"ready\" : \"loading\",\n );\n const [error, setError] = useState<Error | null>(null);\n const [localSearch, setLocalSearch] = useState(\"\");\n const [filter, setFilter] = useState<Filter>(\"all\");\n const [developerMode, setDeveloperMode] = useState(false);\n\n const query = search ?? localSearch;\n const activeActions = useMemo(() => {\n if (filters?.actions?.length) {\n return filters.actions;\n }\n\n return filter === \"all\" ? undefined : filterActions[filter];\n }, [filter, filters?.actions]);\n\n useEffect(() => {\n if (controlledEntries) {\n setEntries(controlledEntries);\n setStatus(\"ready\");\n return;\n }\n\n let cancelled = false;\n setStatus(\"loading\");\n setError(null);\n const queryOptions: ActivityPanelQuery = {\n resource,\n search: query,\n actions: activeActions,\n actor: filters?.actor,\n from: filters?.from,\n to: filters?.to,\n };\n onQueryChange?.(queryOptions);\n\n activity\n .query(queryOptions)\n .then((nextEntries) => {\n if (!cancelled) {\n setEntries(nextEntries);\n setStatus(\"ready\");\n }\n })\n .catch((unknownError: unknown) => {\n const nextError =\n unknownError instanceof Error\n ? unknownError\n : new ActivityError(\"UNKNOWN_ERROR\", \"Activity could not load\");\n\n if (!cancelled) {\n setError(nextError);\n setStatus(\"error\");\n onError?.(nextError);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activity,\n activeActions,\n controlledEntries,\n filters?.actor,\n filters?.from,\n filters?.to,\n onError,\n onQueryChange,\n query,\n resource,\n ]);\n\n return (\n <section\n className={`activity-panel activity-panel--${variant}`}\n aria-label={messages.panelLabel}\n >\n <header className=\"activity-panel__header\">\n <div>\n <p className=\"eyebrow\">{resource.type}</p>\n <h1>{messages.title}</h1>\n </div>\n <label className=\"developer-toggle\">\n <input\n checked={developerMode}\n onChange={(event) => setDeveloperMode(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {messages.developer}\n </label>\n </header>\n\n <div className=\"activity-toolbar\">\n <label className=\"search-field\">\n <span aria-hidden=\"true\">/</span>\n <input\n aria-label={messages.searchLabel}\n disabled={search !== undefined}\n onChange={(event) => setLocalSearch(event.currentTarget.value)}\n placeholder={messages.searchPlaceholder}\n value={query}\n />\n </label>\n <div className=\"segmented-control\" aria-label={messages.filterLabel}>\n {(Object.keys(actionLabels) as Filter[]).map((key) => (\n <button\n aria-pressed={filter === key}\n className={filter === key ? \"is-active\" : \"\"}\n disabled={Boolean(filters?.actions?.length)}\n key={key}\n onClick={() => setFilter(key)}\n type=\"button\"\n >\n {actionLabels[key]}\n </button>\n ))}\n </div>\n </div>\n\n {status === \"loading\" ? <ActivityLoading messages={messages} /> : null}\n {status === \"error\" ? <ActivityErrorState error={error} messages={messages} /> : null}\n {status === \"ready\" && entries.length === 0 ? (\n <ActivityEmpty hasQuery={query.trim().length > 0 || filter !== \"all\"} messages={messages} />\n ) : null}\n\n {status === \"ready\" && entries.length > 0 ? (\n <div className=\"activity-stream\">\n {entries.map((entry) => (\n <ActivityEntryRow\n developerMode={developerMode}\n entry={entry}\n key={entry.id}\n onEntryClick={onEntryClick}\n messages={messages}\n locale={locale}\n />\n ))}\n </div>\n ) : null}\n </section>\n );\n}\n\nfunction ActivityEntryRow({\n developerMode,\n entry,\n onEntryClick,\n messages,\n locale,\n}: {\n developerMode: boolean;\n entry: ActivityRecord;\n onEntryClick?: (entry: ActivityRecord) => void;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n const summary = getEntrySummary(entry, messages);\n return (\n <article className=\"activity-entry\">\n <button\n aria-expanded={expanded}\n className=\"entry-main\"\n onClick={() => {\n setExpanded((value) => !value);\n onEntryClick?.(entry);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setExpanded(false);\n return;\n }\n\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n const stream = event.currentTarget.closest(\".activity-stream\");\n const toggles = Array.from(\n stream?.querySelectorAll<HTMLButtonElement>(\"button.entry-main\") ?? [],\n );\n const currentIndex = toggles.indexOf(event.currentTarget);\n const nextIndex =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? toggles.length - 1\n : event.key === \"ArrowDown\"\n ? Math.min(currentIndex + 1, toggles.length - 1)\n : Math.max(currentIndex - 1, 0);\n\n if (toggles[nextIndex]) {\n event.preventDefault();\n toggles[nextIndex].focus();\n }\n }\n }}\n type=\"button\"\n >\n <span className={`entry-icon entry-icon--${summary.family}`}>\n <span className={`gg-icon gg-icon--${summary.icon}`} aria-hidden=\"true\" />\n </span>\n <span className=\"entry-body\">\n <span className=\"entry-kicker\">{summary.title}</span>\n <EntryContent entry={entry} expanded={expanded} messages={messages} locale={locale} />\n <span className=\"entry-meta\">\n {entry.actor.name} <span aria-hidden=\"true\">-</span>{\" \"}\n {formatTime(entry.timestamp, locale)}\n </span>\n </span>\n <span className=\"expand-indicator\" aria-hidden=\"true\">\n <span\n className={`gg-icon gg-icon--chevron ${expanded ? \"is-open\" : \"\"}`}\n />\n </span>\n </button>\n <div className=\"entry-actions\" aria-label={messages.entryActionsLabel}>\n <button\n aria-label={messages.copyIdLabel}\n onClick={() => void copyText(entry.id)}\n title={messages.copyIdTitle}\n type=\"button\"\n >\n <span className=\"gg-icon gg-icon--copy\" aria-hidden=\"true\" />\n </button>\n </div>\n\n {expanded ? (\n <div className=\"entry-details\">\n <dl>\n <div>\n <dt>{messages.changedBy}</dt>\n <dd>\n {entry.actor.name} <span>{entry.actor.type}</span>\n </dd>\n </div>\n <div>\n <dt>{messages.at}</dt>\n <dd>{formatFullDate(entry.timestamp, locale)}</dd>\n </div>\n <div>\n <dt>{messages.source}</dt>\n <dd>{readMetadata(entry, \"source\") ?? messages.application}</dd>\n </div>\n <div>\n <dt>{messages.version}</dt>\n <dd>{readMetadata(entry, \"version\") ?? messages.unversioned}</dd>\n </div>\n </dl>\n\n {developerMode ? (\n <pre className=\"developer-json\">\n {JSON.stringify(serializeEntry(entry), null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </article>\n );\n}\n\nfunction EntryContent({\n entry,\n expanded,\n messages,\n locale,\n}: {\n entry: ActivityRecord;\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n if (entry.action === \"update\") {\n return <UpdateChanges changes={entry.changes ?? []} expanded={expanded} messages={messages} locale={locale} />;\n }\n\n if (entry.action === \"comment\" && entry.content?.type === \"comment\") {\n return (\n <span className=\"content-preview\">\n <q>{entry.content.text}</q>\n </span>\n );\n }\n\n if (entry.action === \"attachment\" && entry.content?.type === \"attachment\") {\n return (\n <span className=\"content-preview\">\n <strong>{entry.content.fileName}</strong>\n <small>\n {formatBytes(entry.content.size)} - {entry.content.mimeType}\n </small>\n </span>\n );\n }\n\n return <span className=\"content-preview\">{getLifecycleBody(entry, messages)}</span>;\n}\n\nfunction UpdateChanges({\n changes,\n expanded,\n messages,\n locale,\n}: {\n changes: readonly Readonly<Change>[];\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const visibleChanges = expanded ? changes : changes.slice(0, 3);\n const hiddenCount = changes.length - visibleChanges.length;\n\n return (\n <span className=\"changes-list\">\n {visibleChanges.map((change) => (\n <span className=\"change-row\" key={`${change.field}-${change.label}`}>\n <span>{change.label}</span>\n <span>\n <del>{formatValue(change.before, messages, locale)}</del>\n <ins>{formatValue(change.after, messages, locale)}</ins>\n </span>\n </span>\n ))}\n {hiddenCount > 0 ? (\n <span className=\"more-changes\">+{hiddenCount} {messages.moreChanges}</span>\n ) : null}\n </span>\n );\n}\n\nfunction ActivityLoading({ messages }: { messages: ActivityPanelMessages }) {\n return (\n <div className=\"activity-skeleton\" aria-label={messages.loadingLabel}>\n {Array.from({ length: 4 }, (_, index) => (\n <div className=\"skeleton-row\" key={index}>\n <span />\n <div>\n <b />\n <i />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ActivityEmpty({ hasQuery, messages }: { hasQuery: boolean; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <span aria-hidden=\"true\">-</span>\n <h2>{hasQuery ? messages.noMatches : messages.noActivity}</h2>\n <p>\n {hasQuery\n ? messages.noMatchesHelp\n : messages.noActivityHelp}\n </p>\n </div>\n );\n}\n\nfunction ActivityErrorState({ error, messages }: { error: Error | null; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <h2>{messages.loadError}</h2>\n <p>{error?.message ?? messages.loadErrorFallback}</p>\n </div>\n );\n}\n\nfunction getEntrySummary(entry: ActivityRecord, messages: ActivityPanelMessages): EntrySummary {\n if (entry.action === \"update\") {\n return {\n title:\n (entry.changes?.length ?? 0) > 1\n ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}`\n : entry.changes?.[0]?.label ?? messages.updated,\n icon: \"file-document\",\n family: \"update\",\n };\n }\n\n if (entry.action === \"comment\") {\n return { title: messages.comment, icon: \"comment\", family: \"content\" };\n }\n\n if (entry.action === \"attachment\") {\n return { title: messages.attachment, icon: \"attachment\", family: \"content\" };\n }\n\n return {\n title: titleize(entry.action),\n icon: entry.action === \"archive\" ? \"box\" : \"add\",\n family: \"lifecycle\",\n };\n}\n\nfunction getLifecycleBody(entry: ActivityRecord, messages: ActivityPanelMessages) {\n if (entry.action === \"create\") {\n return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;\n }\n\n const reason = readMetadata(entry, \"reason\");\n return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);\n}\n\nfunction readMetadata(entry: ActivityRecord, key: string) {\n const value = entry.metadata?.[key];\n return typeof value === \"string\" || typeof value === \"number\" ? String(value) : undefined;\n}\n\nfunction formatTime(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction formatFullDate(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date);\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n const kilobytes = bytes / 1024;\n return `${Math.round(kilobytes)} KB`;\n}\n\nfunction formatValue(value: unknown, messages: ActivityPanelMessages, locale?: string) {\n if (value === null || value === undefined) {\n return messages.none;\n }\n\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(value);\n }\n\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : JSON.stringify(value);\n}\n\nfunction titleize(value: string) {\n return value\n .replace(/[._-]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction serializeEntry(entry: ActivityRecord) {\n return {\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n };\n}\n\nasync function copyText(value: string) {\n if (globalThis.navigator?.clipboard) {\n await globalThis.navigator.clipboard.writeText(value);\n }\n}\n", "export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.activity-panel *,.activity-panel *:before,.activity-panel *:after{box-sizing:border-box}.activity-panel button,.activity-panel input{font:inherit}.activity-panel button{cursor:pointer}.activity-panel .eyebrow{margin:0 0 7px;color:#68717c;font-size:.72rem;font-weight:700;text-transform:uppercase}.segmented-control{display:flex;flex-wrap:wrap;gap:4px;padding:3px;border:1px solid #d0d7de;border-radius:8px;background:#f1f3f5}.segmented-control button{min-height:29px;border:0;border-radius:6px;background:transparent;color:#5f6b76;font-size:.78rem;font-weight:650;padding:0 10px}.segmented-control button.is-active{background:#fff;color:#1f2328;box-shadow:0 1px 1px #1b1f2414}.activity-panel{width:100%}.activity-panel__header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 18px 14px;border-bottom:1px solid #e5e7eb}.activity-panel__header h1{margin:0;color:#111827;font-size:1.05rem;font-weight:740;letter-spacing:0}.developer-toggle{display:inline-flex;align-items:center;gap:8px;color:#5f6b76;font-size:.78rem;font-weight:650;white-space:nowrap}.developer-toggle input{width:15px;height:15px;accent-color:#24775e}.activity-toolbar{display:grid;grid-template-columns:minmax(170px,1fr) auto;gap:10px;padding:12px 18px;border-bottom:1px solid #e5e7eb}.search-field{display:flex;min-width:0;height:36px;align-items:center;gap:9px;border:1px solid #d0d7de;border-radius:7px;background:#fff;color:#6b7280;padding:0 11px}.search-field input{width:100%;min-width:0;border:0;outline:0;color:#24292f}.search-field input::placeholder{color:#8c959f}.activity-stream{padding:0 0 10px}.activity-entry{position:relative;border-top:1px solid #edf0f2}.activity-entry:first-child{border-top:0}.entry-main{display:grid;grid-template-columns:28px minmax(0,1fr) 18px;gap:11px;width:100%;min-height:76px;border:0;background:transparent;color:inherit;padding:13px 58px 13px 18px;text-align:left}.entry-main:hover,.entry-main:focus-visible{background:#f8fafb}.entry-main:focus-visible{outline:2px solid #24775e;outline-offset:-2px}.entry-actions{position:absolute;top:12px;right:14px;display:flex;gap:5px;opacity:0;pointer-events:none;transition:opacity .14s ease}.activity-entry:hover .entry-actions,.activity-entry:focus-within .entry-actions{opacity:1;pointer-events:auto}.entry-actions button{display:inline-flex;min-width:28px;height:26px;align-items:center;justify-content:center;border:1px solid #d0d7de;border-radius:6px;background:#fff;color:#4b5563;font-size:.72rem;font-weight:750}.entry-actions button:hover,.entry-actions button:focus-visible{border-color:#9aa4af;background:#f6f8fa;outline:none}.gg-icon{--icon-size: 16px;width:var(--icon-size);height:var(--icon-size);display:inline-block;background:currentColor;color:currentColor;flex:0 0 auto;-webkit-mask:var(--gg-icon) center / contain no-repeat;mask:var(--gg-icon) center / contain no-repeat}.entry-actions .gg-icon,.expand-indicator .gg-icon{--icon-size: 13px}.gg-icon--chevron{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path%0A d="M10.5858 6.34317L12 4.92896L19.0711 12L12 19.0711L10.5858 17.6569L16.2427 12L10.5858 6.34317Z"%0A fill="currentColor"%0A />%0A</svg>');transition:transform .14s ease}.gg-icon--chevron.is-open{transform:rotate(90deg)}.gg-icon--add{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4Z"%0A fill="currentColor"%0A />%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M13 7C13 6.44772 12.5523 6 12 6C11.4477 6 11 6.44772 11 7V11H7C6.44772 11 6 11.4477 6 12C6 12.5523 6.44772 13 7 13H11V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V13H17C17.5523 13 18 12.5523 18 12C18 11.4477 17.5523 11 17 11H13V7Z"%0A fill="currentColor"%0A />%0A</svg>%0A')}.gg-icon--attachment{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path%0A d="M14 0C16.7614 0 19 2.23858 19 5V17C19 20.866 15.866 24 12 24C8.13401 24 5 20.866 5 17V9H7V17C7 19.7614 9.23858 22 12 22C14.7614 22 17 19.7614 17 17V5C17 3.34315 15.6569 2 14 2C12.3431 2 11 3.34315 11 5V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V6H15V17C15 18.6569 13.6569 20 12 20C10.3431 20 9 18.6569 9 17V5C9 2.23858 11.2386 0 14 0Z"%0A fill="currentColor"%0A />%0A</svg>')}.gg-icon--box{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path%0A d="M10 12C9.44769 12 9 12.4477 9 13C9 13.5523 9.44769 14 10 14H14C14.5522 14 15 13.5523 15 13C15 12.4477 14.5522 12 14 12H10Z"%0A fill="currentColor"%0A />%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M4 2C2.34314 2 1 3.34314 1 5V19C1 20.6569 2.34314 22 4 22H20C21.6569 22 23 20.6569 23 19V5C23 3.34314 21.6569 2 20 2H4ZM20 4H4C3.44769 4 3 4.44769 3 5V8H21V5C21 4.44769 20.5522 4 20 4ZM3 19V10H21V19C21 19.5523 20.5522 20 20 20H4C3.44769 20 3 19.5523 3 19Z"%0A fill="currentColor"%0A />%0A</svg>')}.gg-icon--comment{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path d="M17 9H7V7H17V9Z" fill="currentColor" />%0A <path d="M7 13H17V11H7V13Z" fill="currentColor" />%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M2 18V2H22V18H16V22H14C11.7909 22 10 20.2091 10 18H2ZM12 16V18C12 19.1046 12.8954 20 14 20V16H20V4H4V16H12Z"%0A fill="currentColor"%0A />%0A</svg>')}.gg-icon--copy{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path d="M13 7H7V5H13V7Z" fill="currentColor" />%0A <path d="M13 11H7V9H13V11Z" fill="currentColor" />%0A <path d="M7 15H13V13H7V15Z" fill="currentColor" />%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z"%0A fill="currentColor"%0A />%0A</svg>')}.gg-icon--file-document{--gg-icon: url('data:image/svg+xml,<svg%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A xmlns="http://www.w3.org/2000/svg"%0A>%0A <path d="M7 18H17V16H7V18Z" fill="currentColor" />%0A <path d="M17 14H7V12H17V14Z" fill="currentColor" />%0A <path d="M7 10H11V8H7V10Z" fill="currentColor" />%0A <path%0A fill-rule="evenodd"%0A clip-rule="evenodd"%0A d="M6 2C4.34315 2 3 3.34315 3 5V19C3 20.6569 4.34315 22 6 22H18C19.6569 22 21 20.6569 21 19V9C21 5.13401 17.866 2 14 2H6ZM6 4H13V9H19V19C19 19.5523 18.5523 20 18 20H6C5.44772 20 5 19.5523 5 19V5C5 4.44772 5.44772 4 6 4ZM15 4.10002C16.6113 4.4271 17.9413 5.52906 18.584 7H15V4.10002Z"%0A fill="currentColor"%0A />%0A</svg>')}.entry-icon{display:inline-flex;width:26px;height:26px;align-items:center;justify-content:center;border:1px solid #d8dee4;border-radius:7px;background:#fff;color:#68717c}.entry-icon--update{color:#24775e}.entry-icon--content{color:#4b5f8f}.entry-icon--lifecycle{color:#8b5e34}.entry-body{display:grid;min-width:0;gap:6px}.entry-kicker{overflow-wrap:anywhere;color:#1f2328;font-size:.9rem;font-weight:720;line-height:1.25}.entry-meta{color:#6b7280;font-size:.78rem;line-height:1.3}.expand-indicator{display:inline-flex;align-items:center;justify-content:center;color:#8c959f}.changes-list{display:grid;gap:6px}.change-row{display:grid;grid-template-columns:minmax(76px,132px) minmax(0,1fr);gap:14px;color:#374151;font-size:.84rem;line-height:1.35}.change-row>span:first-child{color:#6b7280}.activity-panel del,ins{text-decoration:none}del{color:#8c959f}.activity-panel del:after{color:#b6bec7;content:" -> "}ins{color:#1f2328;font-weight:650}.more-changes{color:#5f6b76;font-size:.8rem;font-weight:650}.content-preview{display:grid;gap:4px;color:#374151;font-size:.88rem;line-height:1.45}.content-preview q{quotes:'"' '"'}.content-preview strong{font-size:.88rem}.content-preview small{color:#6b7280;font-size:.76rem}.entry-details{margin-left:57px;padding:0 18px 15px 0}.entry-details dl{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0;border-top:1px solid #edf0f2;padding-top:13px}.entry-details div{min-width:0}.entry-details dt{margin-bottom:4px;color:#6b7280;font-size:.7rem;font-weight:750;text-transform:uppercase}.entry-details dd{margin:0;overflow-wrap:anywhere;color:#24292f;font-size:.82rem}.entry-details dd span{color:#6b7280}.developer-json{overflow-x:auto;margin:13px 0 0;border:1px solid #d8dee4;border-radius:8px;background:#f6f8fa;color:#24292f;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:.7rem;line-height:1.55;padding:11px}.empty-state{display:grid;justify-items:center;gap:8px;padding:52px 22px;color:#6b7280;text-align:center}.empty-state h2{margin:0;color:#24292f;font-size:.98rem;font-weight:720}.empty-state p{margin:0;max-width:280px;font-size:.85rem;line-height:1.45}.activity-skeleton{display:grid;gap:0;padding-bottom:10px}.skeleton-row{display:grid;grid-template-columns:28px minmax(0,1fr);gap:11px;padding:15px 18px;border-top:1px solid #edf0f2}.skeleton-row span,.skeleton-row b,.skeleton-row i{display:block;border-radius:7px;background:linear-gradient(90deg,#eef1f4,#f8fafb,#eef1f4);background-size:200% 100%;animation:shimmer 1.2s infinite linear}.skeleton-row span{width:26px;height:26px}.skeleton-row div{display:grid;gap:9px}.skeleton-row b{width:min(220px,70%);height:13px}.skeleton-row i{width:min(360px,100%);height:11px}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media(max-width:620px){.activity-toolbar,.activity-panel__header{align-items:stretch;flex-direction:column}.activity-toolbar{display:grid;grid-template-columns:1fr}.segmented-control button{flex:1 1 auto}.entry-main{grid-template-columns:26px minmax(0,1fr) 18px;padding-right:78px;padding-left:14px}.change-row{grid-template-columns:1fr;gap:3px}.entry-details{margin-left:51px;padding-right:14px}.entry-details dl{grid-template-columns:1fr 1fr}}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
create table if not exists activity_entries (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
resource_type text not null,
|
|
4
|
+
resource_id text not null,
|
|
5
|
+
resource_title text,
|
|
6
|
+
action text not null,
|
|
7
|
+
actor_type text not null,
|
|
8
|
+
actor_id text not null,
|
|
9
|
+
actor_name text not null,
|
|
10
|
+
actor_avatar_url text,
|
|
11
|
+
content_type text,
|
|
12
|
+
content_json jsonb,
|
|
13
|
+
metadata_json jsonb,
|
|
14
|
+
created_at timestamptz not null
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
create table if not exists activity_changes (
|
|
18
|
+
id uuid primary key,
|
|
19
|
+
entry_id uuid not null references activity_entries(id) on delete cascade,
|
|
20
|
+
position integer not null check (position >= 0),
|
|
21
|
+
field text not null,
|
|
22
|
+
label text not null,
|
|
23
|
+
before_value jsonb,
|
|
24
|
+
after_value jsonb,
|
|
25
|
+
value_type text not null
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
create index if not exists activity_entries_resource_created_idx
|
|
29
|
+
on activity_entries (resource_type, resource_id, created_at desc);
|
|
30
|
+
|
|
31
|
+
create index if not exists activity_entries_actor_idx
|
|
32
|
+
on activity_entries (actor_id);
|
|
33
|
+
|
|
34
|
+
create index if not exists activity_entries_action_idx
|
|
35
|
+
on activity_entries (action);
|
|
36
|
+
|
|
37
|
+
create index if not exists activity_entries_created_idx
|
|
38
|
+
on activity_entries (created_at desc);
|
|
39
|
+
|
|
40
|
+
create index if not exists activity_entries_metadata_gin_idx
|
|
41
|
+
on activity_entries using gin (metadata_json);
|
|
42
|
+
|
|
43
|
+
create index if not exists activity_entries_content_gin_idx
|
|
44
|
+
on activity_entries using gin (content_json);
|