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