strapi-plugin-timeline 0.0.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.
@@ -0,0 +1,559 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const admin = require("@strapi/strapi/admin");
5
+ const reactRouterDom = require("react-router-dom");
6
+ const React = require("react");
7
+ const designSystem = require("@strapi/design-system");
8
+ const icons = require("@strapi/icons");
9
+ const index = require("./index-BPC9ghPd.js");
10
+ const ACTION_COLORS = {
11
+ create: { bg: "success100", text: "success700" },
12
+ update: { bg: "warning100", text: "warning700" },
13
+ publish: { bg: "primary100", text: "primary700" },
14
+ delete: { bg: "danger100", text: "danger700" },
15
+ clean: { bg: "danger100", text: "danger700" },
16
+ restore: { bg: "primary100", text: "primary700" }
17
+ };
18
+ const HomePage = () => {
19
+ const { get, del, put, post } = admin.useFetchClient();
20
+ const { toggleNotification } = admin.useNotification();
21
+ const { user: currentUser } = admin.useAuth("Timeline", (auth) => ({ user: auth.user }));
22
+ const [searchParams, setSearchParams] = reactRouterDom.useSearchParams();
23
+ const selectedContentTypes = searchParams.getAll("contentType");
24
+ const selectedUsers = searchParams.getAll("userId");
25
+ const selectedActions = searchParams.getAll("action");
26
+ const entryDocumentId = searchParams.get("entryDocumentId") || "";
27
+ const selectedLocales = searchParams.getAll("locale");
28
+ const dateFrom = searchParams.get("dateFrom") || "";
29
+ const dateTo = searchParams.get("dateTo") || "";
30
+ const sort = searchParams.get("sort") || "createdAt";
31
+ const sortOrder = searchParams.get("sortOrder") || "desc";
32
+ const page = parseInt(searchParams.get("page") || "1", 10);
33
+ const [entries, setEntries] = React.useState([]);
34
+ const [contentTypes, setContentTypes] = React.useState([]);
35
+ const [users, setUsers] = React.useState([]);
36
+ const [pagination, setPagination] = React.useState({
37
+ page: 1,
38
+ pageSize: 20,
39
+ total: 0,
40
+ pageCount: 0
41
+ });
42
+ const [loading, setLoading] = React.useState(true);
43
+ const [viewEntry, setViewEntry] = React.useState(null);
44
+ const [canReadUsers, setCanReadUsers] = React.useState(false);
45
+ const [showCleanDialog, setShowCleanDialog] = React.useState(false);
46
+ const [cleanContentTypes, setCleanContentTypes] = React.useState([]);
47
+ const [isCleaning, setIsCleaning] = React.useState(false);
48
+ const [restoreTarget, setRestoreTarget] = React.useState(null);
49
+ const [isRestoring, setIsRestoring] = React.useState(false);
50
+ const [currentDocForRestore, setCurrentDocForRestore] = React.useState(null);
51
+ const [fetchingCurrentDoc, setFetchingCurrentDoc] = React.useState(false);
52
+ const [docIdSearch, setDocIdSearch] = React.useState(entryDocumentId);
53
+ const [locales, setLocales] = React.useState([]);
54
+ React.useEffect(() => {
55
+ const checkPermissions = async () => {
56
+ try {
57
+ const res = await get("/admin/permissions/check", {
58
+ params: { permissions: [{ action: "admin::users.read" }] }
59
+ });
60
+ setCanReadUsers(res?.data?.data?.[0]?.hasPermission ?? false);
61
+ } catch {
62
+ setCanReadUsers(false);
63
+ }
64
+ };
65
+ checkPermissions();
66
+ }, [get]);
67
+ React.useEffect(() => {
68
+ loadContentTypes();
69
+ loadUsers();
70
+ loadLocales();
71
+ }, []);
72
+ React.useEffect(() => {
73
+ loadEntries();
74
+ }, [searchParams]);
75
+ React.useEffect(() => {
76
+ if (!restoreTarget) {
77
+ setCurrentDocForRestore(null);
78
+ return;
79
+ }
80
+ let cancelled = false;
81
+ const fetchCurrentDoc = async () => {
82
+ setFetchingCurrentDoc(true);
83
+ setCurrentDocForRestore(null);
84
+ try {
85
+ const ct = contentTypes.find((c) => c.uid === restoreTarget.contentType);
86
+ const kind = ct?.kind || "collectionType";
87
+ let url = kind === "singleType" ? `/content-manager/single-types/${restoreTarget.contentType}` : `/content-manager/collection-types/${restoreTarget.contentType}/${restoreTarget.entryDocumentId}`;
88
+ if (restoreTarget.locale) url += `?locale=${restoreTarget.locale}`;
89
+ const resp = await get(url);
90
+ if (!cancelled) setCurrentDocForRestore(resp.data?.data || resp.data || null);
91
+ } catch {
92
+ if (!cancelled) setCurrentDocForRestore(null);
93
+ } finally {
94
+ if (!cancelled) setFetchingCurrentDoc(false);
95
+ }
96
+ };
97
+ fetchCurrentDoc();
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, [restoreTarget]);
102
+ const updateParams = React.useCallback(
103
+ (updates) => {
104
+ setSearchParams((prev) => {
105
+ const next = new URLSearchParams();
106
+ prev.forEach((value, key) => {
107
+ if (!(key in updates)) next.append(key, value);
108
+ });
109
+ for (const [key, val] of Object.entries(updates)) {
110
+ if (val === null || val === "" || Array.isArray(val) && val.length === 0) continue;
111
+ if (Array.isArray(val)) {
112
+ val.forEach((v) => next.append(key, v));
113
+ } else {
114
+ next.set(key, val);
115
+ }
116
+ }
117
+ return next;
118
+ });
119
+ },
120
+ [setSearchParams]
121
+ );
122
+ const loadContentTypes = async () => {
123
+ try {
124
+ const res = await get(`/${index.PLUGIN_ID}/content-types`);
125
+ setContentTypes(res.data.data || []);
126
+ } catch {
127
+ toggleNotification({ type: "danger", message: "Failed to load content types." });
128
+ }
129
+ };
130
+ const loadUsers = async () => {
131
+ try {
132
+ const res = await get(`/${index.PLUGIN_ID}/entries/users`);
133
+ setUsers(res.data.data || []);
134
+ } catch {
135
+ }
136
+ };
137
+ const loadLocales = async () => {
138
+ try {
139
+ const res = await get(`/${index.PLUGIN_ID}/entries/locales`);
140
+ setLocales(res.data.data || []);
141
+ } catch {
142
+ }
143
+ };
144
+ const loadEntries = async () => {
145
+ setLoading(true);
146
+ try {
147
+ const params = new URLSearchParams();
148
+ params.set("page", String(page));
149
+ params.set("pageSize", "20");
150
+ params.set("sort", sort);
151
+ params.set("sortOrder", sortOrder);
152
+ selectedContentTypes.forEach((ct) => params.append("contentType", ct));
153
+ selectedUsers.forEach((u) => params.append("userId", u));
154
+ selectedActions.forEach((a) => params.append("action", a));
155
+ selectedLocales.forEach((l) => params.append("locale", l));
156
+ if (entryDocumentId) params.set("entryDocumentId", entryDocumentId);
157
+ if (dateFrom) params.set("dateFrom", dateFrom);
158
+ if (dateTo) params.set("dateTo", dateTo);
159
+ const res = await get(`/${index.PLUGIN_ID}/entries?${params.toString()}`);
160
+ setEntries(res.data.data || []);
161
+ setPagination(res.data.meta?.pagination || pagination);
162
+ } catch {
163
+ toggleNotification({ type: "danger", message: "Failed to load timeline entries." });
164
+ } finally {
165
+ setLoading(false);
166
+ }
167
+ };
168
+ const getDisplayName = (uid) => {
169
+ const ct = contentTypes.find((c) => c.uid === uid);
170
+ return ct?.displayName || uid.split(".").pop() || uid;
171
+ };
172
+ const getContentTypeKind = (uid) => {
173
+ const ct = contentTypes.find((c) => c.uid === uid);
174
+ return ct?.kind || "collectionType";
175
+ };
176
+ const getContentManagerLink = (entry) => {
177
+ const kind = getContentTypeKind(entry.contentType);
178
+ if (kind === "singleType") {
179
+ return `/content-manager/single-types/${entry.contentType}`;
180
+ }
181
+ return `/content-manager/collection-types/${entry.contentType}/${entry.entryDocumentId}`;
182
+ };
183
+ const formatDate2 = (dateStr) => new Date(dateStr).toLocaleString();
184
+ const getUserLink = (entry) => {
185
+ if (!entry.userId) return null;
186
+ if (currentUser && entry.userId === currentUser.id) return "/me";
187
+ if (canReadUsers) return `/settings/users/${entry.userId}`;
188
+ return null;
189
+ };
190
+ const handleClean = async () => {
191
+ if (cleanContentTypes.length === 0) {
192
+ toggleNotification({ type: "warning", message: "Please select at least one content type to clean." });
193
+ return;
194
+ }
195
+ setIsCleaning(true);
196
+ try {
197
+ await post(`/${index.PLUGIN_ID}/entries/clean`, { contentTypes: cleanContentTypes });
198
+ toggleNotification({ type: "success", message: "Selected timeline entries cleaned." });
199
+ setShowCleanDialog(false);
200
+ setCleanContentTypes([]);
201
+ loadEntries();
202
+ loadUsers();
203
+ } catch {
204
+ toggleNotification({ type: "danger", message: "Failed to clean timeline entries." });
205
+ } finally {
206
+ setIsCleaning(false);
207
+ }
208
+ };
209
+ const handleRestore = async (entry) => {
210
+ setIsRestoring(true);
211
+ try {
212
+ const snapshot = entry.content;
213
+ if (!snapshot || typeof snapshot !== "object") {
214
+ toggleNotification({ type: "danger", message: "Invalid snapshot data." });
215
+ return;
216
+ }
217
+ const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
218
+ "id",
219
+ "documentId",
220
+ "createdAt",
221
+ "updatedAt",
222
+ "publishedAt",
223
+ "createdBy",
224
+ "updatedBy",
225
+ "created_by_id",
226
+ "updated_by_id",
227
+ "created_at",
228
+ "updated_at",
229
+ "published_at",
230
+ "locale",
231
+ "localizations"
232
+ ]);
233
+ const restoreData = {};
234
+ for (const [key, value] of Object.entries(snapshot)) {
235
+ if (!SYSTEM_FIELDS.has(key)) {
236
+ restoreData[key] = value;
237
+ }
238
+ }
239
+ await post(`/${index.PLUGIN_ID}/entries/snapshot-before-restore`, {
240
+ contentType: entry.contentType,
241
+ entryDocumentId: entry.entryDocumentId,
242
+ locale: entry.locale || null,
243
+ restoreData
244
+ });
245
+ toggleNotification({
246
+ type: "success",
247
+ message: "Entry restored from timeline snapshot."
248
+ });
249
+ loadEntries();
250
+ } catch (err) {
251
+ toggleNotification({
252
+ type: "danger",
253
+ message: err?.response?.data?.error?.message || err?.message || "Failed to restore entry."
254
+ });
255
+ } finally {
256
+ setIsRestoring(false);
257
+ setRestoreTarget(null);
258
+ }
259
+ };
260
+ const handleSort = (field) => {
261
+ const newOrder = sort === field && sortOrder === "desc" ? "asc" : "desc";
262
+ updateParams({ sort: field, sortOrder: newOrder, page: "1" });
263
+ };
264
+ const handlePageChange = (newPage) => {
265
+ updateParams({ page: String(newPage) });
266
+ };
267
+ const renderSortIcon = (field) => {
268
+ if (sort !== field) return null;
269
+ return sortOrder === "asc" ? /* @__PURE__ */ jsxRuntime.jsx(icons.CaretUp, { width: 10, height: 10 }) : /* @__PURE__ */ jsxRuntime.jsx(icons.CaretDown, { width: 10, height: 10 });
270
+ };
271
+ const SortHeader = ({ field, children }) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsxs(
272
+ "button",
273
+ {
274
+ type: "button",
275
+ onClick: () => handleSort(field),
276
+ style: {
277
+ background: "none",
278
+ border: "none",
279
+ cursor: "pointer",
280
+ display: "inline-flex",
281
+ alignItems: "center",
282
+ gap: "4px",
283
+ padding: 0
284
+ },
285
+ children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children }),
287
+ renderSortIcon(field)
288
+ ]
289
+ }
290
+ ) });
291
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { padding: 8, children: [
292
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingBottom: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-start", children: [
293
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 3, alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", children: "Timeline" }),
295
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: "Content history log across tracked content types." })
296
+ ] }) }),
297
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}), onClick: () => setShowCleanDialog(true), children: "Clean" })
298
+ ] }) }),
299
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
300
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, xs: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content Type" }),
302
+ /* @__PURE__ */ jsxRuntime.jsx(
303
+ designSystem.MultiSelect,
304
+ {
305
+ value: selectedContentTypes,
306
+ onChange: (values) => updateParams({ contentType: values, page: "1" }),
307
+ placeholder: "All content types",
308
+ onClear: () => updateParams({ contentType: null, page: "1" }),
309
+ withTags: true,
310
+ children: contentTypes.map((ct) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.MultiSelectOption, { value: ct.uid, children: ct.displayName }, ct.uid))
311
+ }
312
+ )
313
+ ] }) }),
314
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 2, xs: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
315
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Locale" }),
316
+ /* @__PURE__ */ jsxRuntime.jsx(
317
+ designSystem.MultiSelect,
318
+ {
319
+ value: selectedLocales,
320
+ onChange: (values) => updateParams({ locale: values, page: "1" }),
321
+ placeholder: "All locales",
322
+ onClear: () => updateParams({ locale: null, page: "1" }),
323
+ withTags: true,
324
+ children: locales.map((l) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.MultiSelectOption, { value: l, children: l.toUpperCase() }, l))
325
+ }
326
+ )
327
+ ] }) }),
328
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, xs: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
329
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "From date" }),
330
+ /* @__PURE__ */ jsxRuntime.jsx(
331
+ designSystem.DatePicker,
332
+ {
333
+ value: dateFrom ? new Date(dateFrom) : void 0,
334
+ onChange: (date) => updateParams({
335
+ dateFrom: date ? date.toISOString().split("T")[0] : null,
336
+ page: "1"
337
+ }),
338
+ onClear: () => updateParams({ dateFrom: null, page: "1" })
339
+ }
340
+ )
341
+ ] }) }),
342
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, xs: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
343
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "To date" }),
344
+ /* @__PURE__ */ jsxRuntime.jsx(
345
+ designSystem.DatePicker,
346
+ {
347
+ value: dateTo ? new Date(dateTo) : void 0,
348
+ onChange: (date) => updateParams({
349
+ dateTo: date ? date.toISOString().split("T")[0] : null,
350
+ page: "1"
351
+ }),
352
+ onClear: () => updateParams({ dateTo: null, page: "1" })
353
+ }
354
+ )
355
+ ] }) }),
356
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
357
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Document ID" }),
358
+ /* @__PURE__ */ jsxRuntime.jsx(
359
+ designSystem.Combobox,
360
+ {
361
+ value: entryDocumentId || void 0,
362
+ onChange: (value) => {
363
+ setDocIdSearch(value || "");
364
+ updateParams({ entryDocumentId: value || null, page: "1" });
365
+ },
366
+ onClear: () => {
367
+ setDocIdSearch("");
368
+ updateParams({ entryDocumentId: null, page: "1" });
369
+ },
370
+ onTextValueChange: (text) => setDocIdSearch(text),
371
+ placeholder: "Search document ID…",
372
+ allowCustomValue: true,
373
+ noOptionsMessage: () => "No matching IDs",
374
+ children: [...new Set(entries.map((e) => e.entryDocumentId))].filter((id) => !docIdSearch || id.toLowerCase().includes(docIdSearch.toLowerCase())).slice(0, 20).map((id) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: id, children: id }, id))
375
+ }
376
+ )
377
+ ] }) }),
378
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 1, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
379
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Action" }),
380
+ /* @__PURE__ */ jsxRuntime.jsx(
381
+ designSystem.MultiSelect,
382
+ {
383
+ value: selectedActions,
384
+ onChange: (values) => updateParams({ action: values, page: "1" }),
385
+ placeholder: "All actions",
386
+ onClear: () => updateParams({ action: null, page: "1" }),
387
+ withTags: true,
388
+ children: ["create", "update", "publish", "delete", "clean", "restore"].map((a) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.MultiSelectOption, { value: a, children: a }, a))
389
+ }
390
+ )
391
+ ] }) }),
392
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 5, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { width: "100%", children: [
393
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "User" }),
394
+ /* @__PURE__ */ jsxRuntime.jsx(
395
+ designSystem.MultiSelect,
396
+ {
397
+ value: selectedUsers,
398
+ onChange: (values) => updateParams({ userId: values, page: "1" }),
399
+ placeholder: "All users",
400
+ onClear: () => updateParams({ userId: null, page: "1" }),
401
+ withTags: true,
402
+ children: users.map((u) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.MultiSelectOption, { value: String(u.userId), children: u.userName }, u.userId))
403
+ }
404
+ )
405
+ ] }) })
406
+ ] }) }),
407
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", shadow: "tableShadow", borderRadius: "4px", children: [
408
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: 8, rowCount: entries.length + 1, children: [
409
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
410
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { field: "action", children: "Action" }),
411
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { field: "contentType", children: "Content" }),
412
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { field: "userName", children: "User" }),
413
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { field: "createdAt", children: "Date" }),
414
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { justifyItems: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "View" }) }),
415
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { justifyItems: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Restore" }) })
416
+ ] }) }),
417
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: loading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Loading…" }) }) }) }) : entries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "No timeline entries found." }) }) }) }) : entries.map((entry) => {
418
+ const colors = ACTION_COLORS[entry.action] || ACTION_COLORS.update;
419
+ const userLink = getUserLink(entry);
420
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
421
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: colors.bg, textColor: colors.text, children: entry.action }) }),
422
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
423
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "bold", variant: "omega", children: getDisplayName(entry.contentType) }),
424
+ entry.entryDocumentId && entry.action !== "clean" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "primary600", children: /* @__PURE__ */ jsxRuntime.jsxs(
425
+ designSystem.Link,
426
+ {
427
+ tag: reactRouterDom.NavLink,
428
+ to: getContentManagerLink(entry),
429
+ style: { color: "inherit", textDecoration: "none" },
430
+ children: [
431
+ entry.entryDocumentId,
432
+ entry.locale && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "primary100", textColor: "primary500", marginLeft: 2, children: entry.locale ? entry.locale.toUpperCase() : "—" })
433
+ ]
434
+ }
435
+ ) })
436
+ ] }) }),
437
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: userLink ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "omega", textColor: "primary600", children: [
438
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Link, { tag: reactRouterDom.NavLink, to: userLink, style: { color: "inherit", textDecoration: "none" }, children: entry.userName || `User #${entry.userId}` }),
439
+ entry.userId && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "primary100", textColor: "primary500", marginLeft: 2, children: [
440
+ "ID ",
441
+ entry.userId
442
+ ] })
443
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "omega", children: [
444
+ entry.userName || (entry.userId ? `User #${entry.userId}` : "—"),
445
+ entry.userId && ` (ID: ${entry.userId})`
446
+ ] }) }),
447
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", children: formatDate2(entry.createdAt) }) }),
448
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { style: { justifyItems: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.IconButton, { label: "View details", onClick: () => setViewEntry(entry), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Eye, {}) }) }),
449
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { style: { justifyItems: "center" }, children: entry.action !== "delete" && entry.action !== "publish" ? /* @__PURE__ */ jsxRuntime.jsx(
450
+ designSystem.IconButton,
451
+ {
452
+ label: "Restore this snapshot",
453
+ onClick: () => setRestoreTarget(entry),
454
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowClockwise, {})
455
+ }
456
+ ) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral400" }) })
457
+ ] }, entry.id);
458
+ }) })
459
+ ] }),
460
+ pagination.pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
461
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
462
+ pagination.total,
463
+ " entries total"
464
+ ] }),
465
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Pagination, { activePage: pagination.page, pageCount: pagination.pageCount, children: [
466
+ /* @__PURE__ */ jsxRuntime.jsx(
467
+ designSystem.PreviousLink,
468
+ {
469
+ onClick: () => handlePageChange(Math.max(1, pagination.page - 1))
470
+ }
471
+ ),
472
+ Array.from({ length: pagination.pageCount }, (_, i) => i + 1).map((p) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.PageLink, { number: p, onClick: () => handlePageChange(p), children: p }, p)),
473
+ /* @__PURE__ */ jsxRuntime.jsx(
474
+ designSystem.NextLink,
475
+ {
476
+ onClick: () => handlePageChange(Math.min(pagination.pageCount, pagination.page + 1))
477
+ }
478
+ )
479
+ ] })
480
+ ] }) })
481
+ ] }),
482
+ /* @__PURE__ */ jsxRuntime.jsx(
483
+ index.TimelineDetailModal,
484
+ {
485
+ entry: viewEntry,
486
+ open: !!viewEntry,
487
+ onClose: () => setViewEntry(null),
488
+ contentTypes
489
+ }
490
+ ),
491
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Root, { open: !!restoreTarget, onOpenChange: (open) => !open && setRestoreTarget(null), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { children: [
492
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Header, { children: "Restore from Timeline" }),
493
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { children: restoreTarget && /* @__PURE__ */ jsxRuntime.jsx(
494
+ index.RestoreDiffTable,
495
+ {
496
+ currentData: currentDocForRestore,
497
+ snapshotData: restoreTarget.content && typeof restoreTarget.content === "object" ? restoreTarget.content : {},
498
+ loading: fetchingCurrentDoc
499
+ }
500
+ ) }),
501
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
502
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: "Cancel" }) }),
503
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Action, { children: /* @__PURE__ */ jsxRuntime.jsx(
504
+ designSystem.Button,
505
+ {
506
+ variant: "danger-light",
507
+ onClick: () => restoreTarget && handleRestore(restoreTarget),
508
+ loading: isRestoring,
509
+ children: "Restore"
510
+ }
511
+ ) })
512
+ ] })
513
+ ] }) }),
514
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Root, { open: showCleanDialog, onOpenChange: (open) => {
515
+ if (!open) {
516
+ setShowCleanDialog(false);
517
+ setCleanContentTypes([]);
518
+ }
519
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { children: [
520
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Header, { children: "Clean timeline entries" }),
521
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
522
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: 'Select the content types whose timeline entries you want to remove. A "clean" action will be logged for each.' }),
523
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
524
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content Types" }),
525
+ /* @__PURE__ */ jsxRuntime.jsx(
526
+ designSystem.MultiSelect,
527
+ {
528
+ value: cleanContentTypes,
529
+ onChange: (values) => setCleanContentTypes(values),
530
+ placeholder: "Select content types…",
531
+ withTags: true,
532
+ children: contentTypes.map((ct) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.MultiSelectOption, { value: ct.uid, children: ct.displayName }, ct.uid))
533
+ }
534
+ )
535
+ ] }) })
536
+ ] }) }),
537
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
538
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: "Cancel" }) }),
539
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Action, { children: /* @__PURE__ */ jsxRuntime.jsx(
540
+ designSystem.Button,
541
+ {
542
+ variant: "danger",
543
+ onClick: handleClean,
544
+ loading: isCleaning,
545
+ disabled: cleanContentTypes.length === 0,
546
+ children: "Clean"
547
+ }
548
+ ) })
549
+ ] })
550
+ ] }) })
551
+ ] });
552
+ };
553
+ const App = () => {
554
+ return /* @__PURE__ */ jsxRuntime.jsx(admin.Layouts.Root, { children: /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Routes, { children: [
555
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { index: true, element: /* @__PURE__ */ jsxRuntime.jsx(HomePage, {}) }),
556
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { path: "*", element: /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Error, {}) })
557
+ ] }) });
558
+ };
559
+ exports.App = App;