@riducms/admin 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.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/THIRD_PARTY_NOTICES.md +217 -0
  4. package/package.json +62 -0
  5. package/src/app/admin-core-view-route.svelte +89 -0
  6. package/src/app/admin-layout.svelte +457 -0
  7. package/src/app/admin-navigation-progress.svelte +204 -0
  8. package/src/app/admin-page-title.svelte +94 -0
  9. package/src/app/admin-provider-layer.svelte +26 -0
  10. package/src/app/admin-router-root.svelte +9 -0
  11. package/src/app/admin-routes.svelte +116 -0
  12. package/src/app/deferred-redirect.svelte +23 -0
  13. package/src/app/loading-boundary.svelte +70 -0
  14. package/src/app/not-found-route.svelte +29 -0
  15. package/src/app-meta.ts +2 -0
  16. package/src/app.css +467 -0
  17. package/src/app.svelte +88 -0
  18. package/src/assets/ridu-logo-arch.svg +3 -0
  19. package/src/assets/ridu-word-logo.svg +7 -0
  20. package/src/components/brand/ridu-logo.svelte +24 -0
  21. package/src/components/code-block/code-block.svelte +107 -0
  22. package/src/components/json-tree/json-tree-node.svelte +77 -0
  23. package/src/components/json-tree/json-viewer.svelte +113 -0
  24. package/src/components/ui/banner/banner.svelte +41 -0
  25. package/src/components/ui/banner/index.ts +5 -0
  26. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
  27. package/src/components/ui/bulk-action-bar/index.ts +1 -0
  28. package/src/components/ui/button/index.ts +8 -0
  29. package/src/components/ui/calendar/calendar-caption.svelte +72 -0
  30. package/src/components/ui/calendar/calendar-cell.svelte +20 -0
  31. package/src/components/ui/calendar/calendar-day.svelte +27 -0
  32. package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
  33. package/src/components/ui/calendar/calendar-grid.svelte +17 -0
  34. package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
  35. package/src/components/ui/calendar/calendar-header.svelte +20 -0
  36. package/src/components/ui/calendar/calendar-heading.svelte +17 -0
  37. package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
  38. package/src/components/ui/calendar/calendar-month.svelte +16 -0
  39. package/src/components/ui/calendar/calendar-months.svelte +20 -0
  40. package/src/components/ui/calendar/calendar-nav.svelte +20 -0
  41. package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
  42. package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
  43. package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
  44. package/src/components/ui/calendar/calendar.svelte +118 -0
  45. package/src/components/ui/calendar/index.ts +1 -0
  46. package/src/components/ui/checkbox/checkbox.svelte +35 -0
  47. package/src/components/ui/checkbox/index.ts +1 -0
  48. package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
  49. package/src/components/ui/confirmation-dialog/index.ts +1 -0
  50. package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
  51. package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
  52. package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
  53. package/src/components/ui/date-value-control/index.ts +3 -0
  54. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  55. package/src/components/ui/dialog/dialog-content.svelte +52 -0
  56. package/src/components/ui/dialog/dialog-description.svelte +20 -0
  57. package/src/components/ui/dialog/dialog-footer.svelte +20 -0
  58. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  59. package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  62. package/src/components/ui/dialog/dialog.svelte +7 -0
  63. package/src/components/ui/dialog/index.ts +8 -0
  64. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
  65. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  69. package/src/components/ui/dropdown-menu/index.ts +5 -0
  70. package/src/components/ui/empty-state/empty-state.svelte +34 -0
  71. package/src/components/ui/empty-state/index.ts +1 -0
  72. package/src/components/ui/input/index.ts +1 -0
  73. package/src/components/ui/kbd/index.ts +1 -0
  74. package/src/components/ui/kbd/kbd.svelte +24 -0
  75. package/src/components/ui/pagination/index.ts +1 -0
  76. package/src/components/ui/pagination/pagination.svelte +88 -0
  77. package/src/components/ui/popover/index.ts +3 -0
  78. package/src/components/ui/popover/popover-content.svelte +29 -0
  79. package/src/components/ui/popover/popover-trigger.svelte +11 -0
  80. package/src/components/ui/popover/popover.svelte +7 -0
  81. package/src/components/ui/radio-group/index.ts +3 -0
  82. package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
  83. package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
  84. package/src/components/ui/radio-group/radio-group.svelte +19 -0
  85. package/src/components/ui/select/index.ts +4 -0
  86. package/src/components/ui/select/select-content.svelte +38 -0
  87. package/src/components/ui/select/select-item.svelte +40 -0
  88. package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
  89. package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
  90. package/src/components/ui/select/select-trigger.svelte +29 -0
  91. package/src/components/ui/select/select.svelte +11 -0
  92. package/src/components/ui/sheet/index.ts +6 -0
  93. package/src/components/ui/sheet/sheet-close.svelte +7 -0
  94. package/src/components/ui/sheet/sheet-content.svelte +77 -0
  95. package/src/components/ui/sheet/sheet-description.svelte +17 -0
  96. package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
  97. package/src/components/ui/sheet/sheet-title.svelte +17 -0
  98. package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
  99. package/src/components/ui/sheet/sheet.svelte +7 -0
  100. package/src/components/ui/skeleton/index.ts +1 -0
  101. package/src/components/ui/skeleton/skeleton.svelte +31 -0
  102. package/src/components/ui/slider/index.ts +3 -0
  103. package/src/components/ui/slider/slider-content.svelte +59 -0
  104. package/src/components/ui/slider/slider.svelte +35 -0
  105. package/src/components/ui/sonner/index.ts +1 -0
  106. package/src/components/ui/sonner/sonner.svelte +55 -0
  107. package/src/components/ui/status-indicator/index.ts +5 -0
  108. package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
  109. package/src/components/ui/table/index.ts +6 -0
  110. package/src/components/ui/table/table-body.svelte +20 -0
  111. package/src/components/ui/table/table-cell.svelte +23 -0
  112. package/src/components/ui/table/table-head.svelte +23 -0
  113. package/src/components/ui/table/table-header.svelte +20 -0
  114. package/src/components/ui/table/table-row.svelte +23 -0
  115. package/src/components/ui/table/table.svelte +22 -0
  116. package/src/components/ui/tabs/index.ts +8 -0
  117. package/src/components/ui/tabs/tabs-content.svelte +17 -0
  118. package/src/components/ui/tabs/tabs-list.svelte +40 -0
  119. package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
  120. package/src/components/ui/tabs/tabs.svelte +19 -0
  121. package/src/components/ui/textarea/index.ts +1 -0
  122. package/src/components/ui/tooltip/index.ts +4 -0
  123. package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
  124. package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
  125. package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  126. package/src/components/ui/tooltip/tooltip.svelte +7 -0
  127. package/src/core/api/admin-client.ts +45 -0
  128. package/src/core/forms/field-condition.ts +63 -0
  129. package/src/core/forms/form-controller.svelte.ts +369 -0
  130. package/src/core/forms/form-draft-recovery.ts +80 -0
  131. package/src/core/forms/form-schema.ts +609 -0
  132. package/src/core/forms/form-validation.ts +750 -0
  133. package/src/core/i18n/admin-i18n.svelte.ts +364 -0
  134. package/src/core/i18n/localized-schema.ts +111 -0
  135. package/src/core/notifications/notification-center.svelte.ts +69 -0
  136. package/src/core/notifications/validation-toast.svelte +18 -0
  137. package/src/core/plugins/field-registry.ts +60 -0
  138. package/src/core/plugins/row-label-registry.ts +62 -0
  139. package/src/core/preferences/preference-write-queue.ts +107 -0
  140. package/src/core/routing/admin-paths.ts +137 -0
  141. package/src/core/runtime/admin-runtime.svelte.ts +625 -0
  142. package/src/features/auth/account-menu.svelte +159 -0
  143. package/src/features/auth/account-navigation.svelte +44 -0
  144. package/src/features/auth/account-route.svelte +395 -0
  145. package/src/features/auth/account-security-controller.svelte.ts +119 -0
  146. package/src/features/auth/account-security-route.svelte +460 -0
  147. package/src/features/auth/auth-brand.svelte +17 -0
  148. package/src/features/auth/auth-frame.svelte +18 -0
  149. package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
  150. package/src/features/auth/create-first-user-route.svelte +174 -0
  151. package/src/features/auth/forgot-password-route.svelte +78 -0
  152. package/src/features/auth/login-route.svelte +221 -0
  153. package/src/features/auth/request-verification-route.svelte +73 -0
  154. package/src/features/auth/reset-password-route.svelte +107 -0
  155. package/src/features/auth/verify-email-route.svelte +60 -0
  156. package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
  157. package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
  158. package/src/features/collections/collection-list-controller.svelte.ts +717 -0
  159. package/src/features/collections/collection-list-filter-popover.svelte +238 -0
  160. package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
  161. package/src/features/collections/collection-list-results.svelte +410 -0
  162. package/src/features/collections/collection-list-route.svelte +951 -0
  163. package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
  164. package/src/features/collections/collection-table-skeleton.svelte +44 -0
  165. package/src/features/collections/list-workspace.ts +265 -0
  166. package/src/features/dashboard/dashboard-route.svelte +141 -0
  167. package/src/features/development/development-tools.svelte +217 -0
  168. package/src/features/documents/document-api-view-loader.svelte +31 -0
  169. package/src/features/documents/document-api-view.svelte +547 -0
  170. package/src/features/documents/document-controller.svelte.ts +1252 -0
  171. package/src/features/documents/document-field-partition.ts +18 -0
  172. package/src/features/documents/document-field-sections.svelte +46 -0
  173. package/src/features/documents/document-heading.ts +20 -0
  174. package/src/features/documents/document-route-dirty.ts +43 -0
  175. package/src/features/documents/document-route-loader.svelte +22 -0
  176. package/src/features/documents/document-route.svelte +1124 -0
  177. package/src/features/documents/live-preview-panel.svelte +390 -0
  178. package/src/features/documents/live-preview.ts +61 -0
  179. package/src/features/documents/upload-document-preview-loader.svelte +40 -0
  180. package/src/features/documents/upload-document-preview.svelte +525 -0
  181. package/src/features/localization/content-locale-switcher.svelte +93 -0
  182. package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
  183. package/src/features/navigation/admin-command-menu.svelte +245 -0
  184. package/src/features/navigation/admin-resource-groups.ts +72 -0
  185. package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
  186. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
  187. package/src/features/reference-browser/reference-browser.svelte +844 -0
  188. package/src/features/uploads/bulk-upload-route.svelte +624 -0
  189. package/src/features/uploads/bulk-upload.ts +39 -0
  190. package/src/features/uploads/upload-metadata-fields.svelte +112 -0
  191. package/src/features/versions/version-diff.ts +72 -0
  192. package/src/features/versions/version-history-route.svelte +581 -0
  193. package/src/fields/field-clipboard.ts +132 -0
  194. package/src/fields/field-layout.svelte +60 -0
  195. package/src/fields/field-layout.ts +57 -0
  196. package/src/fields/field-messages.svelte +29 -0
  197. package/src/fields/field-renderer.svelte +152 -0
  198. package/src/fields/field-shell.svelte +30 -0
  199. package/src/fields/field-tab-layout.svelte +99 -0
  200. package/src/fields/join/join-field.svelte +358 -0
  201. package/src/fields/localized-field-presentation.ts +17 -0
  202. package/src/fields/nested/nested-field.svelte +602 -0
  203. package/src/fields/nested/scoped-field.ts +78 -0
  204. package/src/fields/nested/sortable-row.svelte +32 -0
  205. package/src/fields/point/point-field.svelte +52 -0
  206. package/src/fields/radio/radio-field.svelte +34 -0
  207. package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
  208. package/src/fields/relationship/relationship-field.svelte +258 -0
  209. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
  210. package/src/fields/relationship/relationship-query.ts +88 -0
  211. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
  212. package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
  213. package/src/fields/relationship/relationship-value.ts +47 -0
  214. package/src/fields/scalar/date-control-value.ts +60 -0
  215. package/src/fields/scalar/date-value.ts +26 -0
  216. package/src/fields/scalar/scalar-field.svelte +137 -0
  217. package/src/fields/select/select-field.svelte +132 -0
  218. package/src/fields/select/select-value.ts +14 -0
  219. package/src/fields/text/slug-field.svelte +87 -0
  220. package/src/fields/text/slug.ts +38 -0
  221. package/src/fields/text/text-field.svelte +41 -0
  222. package/src/fields/ui/ui-field.svelte +12 -0
  223. package/src/fields/virtual/virtual-field.svelte +28 -0
  224. package/src/index.ts +49 -0
  225. package/src/main.ts +12 -0
  226. package/src/vite-env.d.ts +9 -0
@@ -0,0 +1,581 @@
1
+ <script lang="ts" module>
2
+ function withContentLocale(path: string, locale: string | undefined) {
3
+ if (locale === undefined) return path;
4
+ return `${path}?${new URLSearchParams({ locale }).toString()}`;
5
+ }
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import { Link, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
10
+ import type { OperationCapabilities, SchemaCollection } from "@riducms/protocol";
11
+ import { untrack } from "svelte";
12
+ import ArrowLeftIcon from "~icons/lucide/arrow-left";
13
+ import CalendarClockIcon from "~icons/lucide/calendar-clock";
14
+ import RotateCcwIcon from "~icons/lucide/rotate-ccw";
15
+ import XIcon from "~icons/lucide/x";
16
+
17
+ import { Banner } from "@admin/components/ui/banner";
18
+ import { Button, buttonVariants } from "@admin/components/ui/button";
19
+ import { Checkbox } from "@admin/components/ui/checkbox";
20
+ import { DateValueControl } from "@admin/components/ui/date-value-control";
21
+ import { Pagination } from "@admin/components/ui/pagination";
22
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "@admin/components/ui/select";
23
+ import { Skeleton } from "@admin/components/ui/skeleton";
24
+ import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
25
+ import {
26
+ documentIDFromAdminPath,
27
+ documentPath,
28
+ documentVersionPath,
29
+ documentVersionsPath,
30
+ globalPath,
31
+ globalVersionPath,
32
+ globalVersionsPath,
33
+ } from "@admin/core/routing/admin-paths";
34
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
35
+ import type {
36
+ AdminDocument,
37
+ AdminScheduledPublish,
38
+ AdminVersion,
39
+ } from "@admin/core/api/admin-client";
40
+ import { formatVersionValue, versionDiffRows } from "@admin/features/versions/version-diff";
41
+
42
+ const runtime = getAdminRuntime();
43
+ const notifications = getNotificationCenter();
44
+ const navigate = useNavigate();
45
+ const location = $derived(useLocation());
46
+ const params = $derived(useParams<"collection" | "document" | "global" | "revision">());
47
+ const globalResource = $derived(params.global !== undefined);
48
+ const slug = $derived(params.global ?? params.collection ?? "");
49
+ let collectionDocumentID = $state(
50
+ untrack(() => documentIDFromAdminPath(location.pathname) ?? "")
51
+ );
52
+ $effect(() => {
53
+ if (globalResource) return;
54
+ const decoded = documentIDFromAdminPath(location.pathname);
55
+ if (decoded !== undefined) collectionDocumentID = decoded;
56
+ });
57
+ const documentID = $derived(globalResource ? slug : collectionDocumentID);
58
+ const requestedRevision = $derived(parseRevision(params.revision));
59
+ const collection = $derived(
60
+ (globalResource ? runtime.manifest?.globals : runtime.manifest?.collections)?.find(
61
+ (candidate) => candidate.slug === slug
62
+ )
63
+ );
64
+ const localization = $derived(runtime.manifest?.application.localization);
65
+ const requestedLocale = $derived(new URLSearchParams(location.search).get("locale"));
66
+ const contentLocale = $derived(
67
+ localization?.locales.some((locale) => locale.code === requestedLocale)
68
+ ? (requestedLocale ?? localization?.defaultLocale)
69
+ : (runtime.contentLocale ?? localization?.defaultLocale)
70
+ );
71
+ const historyPath = $derived(
72
+ withContentLocale(
73
+ globalResource ? globalVersionsPath(slug) : documentVersionsPath(slug, documentID),
74
+ contentLocale
75
+ )
76
+ );
77
+ const editPath = $derived(
78
+ withContentLocale(
79
+ globalResource ? globalPath(slug) : documentPath(slug, documentID),
80
+ contentLocale
81
+ )
82
+ );
83
+
84
+ let versions = $state.raw<AdminVersion[]>([]);
85
+ let currentDocument = $state.raw<AdminDocument>();
86
+ let operations = $state.raw<OperationCapabilities>();
87
+ let schedules = $state.raw<AdminScheduledPublish[]>([]);
88
+ let loading = $state(true);
89
+ let error = $state<string>();
90
+ let page = $state(1);
91
+ let compareRevision = $state("");
92
+ let modifiedOnly = $state(true);
93
+ let restoring = $state(false);
94
+ let scheduling = $state(false);
95
+ let scheduleAt = $state(defaultScheduleTime());
96
+
97
+ const selectedVersion = $derived(
98
+ requestedRevision === undefined
99
+ ? undefined
100
+ : versions.find((version) => version.Revision === requestedRevision)
101
+ );
102
+ const comparisonVersion = $derived(
103
+ versions.find((version) => version.Revision === Number(compareRevision))
104
+ );
105
+ const comparisonLabel = $derived(
106
+ comparisonVersion === undefined
107
+ ? runtime.i18n.t("versions:noComparison")
108
+ : runtime.i18n.t("versions:revisionWithStatus", {
109
+ revision: runtime.i18n.formatNumber(comparisonVersion.Revision),
110
+ status: comparisonVersion.Status,
111
+ })
112
+ );
113
+ const rows = $derived(
114
+ collection === undefined || selectedVersion === undefined
115
+ ? []
116
+ : versionDiffRows(collection, selectedVersion, comparisonVersion, runtime.i18n)
117
+ );
118
+ const visibleRows = $derived(modifiedOnly ? rows.filter((row) => row.changed) : rows);
119
+ const pageSize = 10;
120
+ const totalPages = $derived(Math.max(1, Math.ceil(versions.length / pageSize)));
121
+ const pageVersions = $derived(versions.slice((page - 1) * pageSize, page * pageSize));
122
+
123
+ $effect(() => {
124
+ const key = `${runtime.manifestRevision}:${slug}:${documentID}:${requestedRevision ?? "list"}:${contentLocale ?? "default"}`;
125
+ if (key.startsWith("0:") || slug === "" || documentID === "") return;
126
+ const request = new AbortController();
127
+ void load(request.signal);
128
+ return () => request.abort();
129
+ });
130
+ $effect(() => {
131
+ if (contentLocale === undefined || requestedLocale === contentLocale) return;
132
+ const next = new URLSearchParams(location.search);
133
+ next.set("locale", contentLocale);
134
+ navigate(`${location.pathname}?${next.toString()}${location.hash}`, { replace: true });
135
+ });
136
+ $effect(() => runtime.registerContentLocaleBlocker(() => restoring || scheduling));
137
+
138
+ async function load(signal: AbortSignal) {
139
+ loading = true;
140
+ error = undefined;
141
+ try {
142
+ const [history, detail, document, access, queued] = await Promise.all([
143
+ globalResource
144
+ ? runtime.client.globalVersions(slug, { signal, locale: contentLocale })
145
+ : runtime.client.versions(slug, documentID, { signal, locale: contentLocale }),
146
+ requestedRevision === undefined
147
+ ? Promise.resolve(undefined)
148
+ : globalResource
149
+ ? runtime.client.globalVersion(slug, requestedRevision, {
150
+ signal,
151
+ locale: contentLocale,
152
+ })
153
+ : runtime.client.version(slug, documentID, requestedRevision, {
154
+ signal,
155
+ locale: contentLocale,
156
+ }),
157
+ globalResource
158
+ ? runtime.client.global(slug, { signal, locale: contentLocale })
159
+ : runtime.client.find(slug, documentID, { signal, locale: contentLocale }),
160
+ globalResource
161
+ ? runtime.client.globalAccess(slug, { signal, locale: contentLocale })
162
+ : runtime.client.collectionAccess(slug, {
163
+ id: documentID,
164
+ signal,
165
+ locale: contentLocale,
166
+ }),
167
+ globalResource
168
+ ? Promise.resolve([])
169
+ : runtime.client.scheduledPublishes(slug, documentID, { signal }),
170
+ ]);
171
+ if (signal.aborted) return;
172
+ versions = [...history].sort((left, right) => right.Revision - left.Revision);
173
+ if (
174
+ detail !== undefined &&
175
+ !versions.some((version) => version.Revision === detail.Revision)
176
+ ) {
177
+ versions = [detail, ...versions].sort((left, right) => right.Revision - left.Revision);
178
+ }
179
+ currentDocument = document;
180
+ operations = access.operations;
181
+ schedules = queued;
182
+ page = Math.min(page, Math.max(1, Math.ceil(versions.length / pageSize)));
183
+ if (detail !== undefined) {
184
+ const index = versions.findIndex((version) => version.Revision === detail.Revision);
185
+ compareRevision = String(
186
+ versions[index + 1]?.Revision ?? versions[index - 1]?.Revision ?? ""
187
+ );
188
+ }
189
+ } catch (cause) {
190
+ if (!signal.aborted) {
191
+ error =
192
+ cause instanceof Error ? cause.message : runtime.i18n.t("versions:historyLoadFailed");
193
+ }
194
+ } finally {
195
+ if (!signal.aborted) loading = false;
196
+ }
197
+ }
198
+
199
+ async function restoreSelected(draft = false) {
200
+ const version = selectedVersion;
201
+ const document = currentDocument;
202
+ if (version === undefined || document === undefined) return;
203
+ const destination = withContentLocale(
204
+ globalResource ? globalPath(slug) : documentPath(slug, documentID),
205
+ contentLocale
206
+ );
207
+ restoring = true;
208
+ try {
209
+ const revision = typeof document._revision === "number" ? document._revision : 0;
210
+ if (globalResource) {
211
+ await runtime.client.restoreGlobal(slug, version.Revision, {
212
+ revision,
213
+ draft,
214
+ locale: contentLocale,
215
+ });
216
+ } else {
217
+ await runtime.client.restore(slug, documentID, version.Revision, {
218
+ revision,
219
+ draft,
220
+ locale: contentLocale,
221
+ });
222
+ }
223
+ runtime.documentsChanged();
224
+ notifications.success({
225
+ title: runtime.i18n.t(
226
+ draft ? "versions:revisionRestoredAsDraft" : "versions:revisionRestored",
227
+ {
228
+ revision: runtime.i18n.formatNumber(version.Revision),
229
+ }
230
+ ),
231
+ message: runtime.i18n.t("versions:previousCurrentPreserved"),
232
+ });
233
+ navigate(destination);
234
+ } catch (cause) {
235
+ notifications.error({
236
+ title: runtime.i18n.t("versions:revisionNotRestored"),
237
+ message:
238
+ cause instanceof Error ? cause.message : runtime.i18n.t("versions:revisionRestoreFailed"),
239
+ });
240
+ } finally {
241
+ restoring = false;
242
+ }
243
+ }
244
+
245
+ async function schedulePublish() {
246
+ const runAt = new Date(scheduleAt);
247
+ if (Number.isNaN(runAt.valueOf()) || runAt <= new Date()) {
248
+ notifications.error({ title: runtime.i18n.t("versions:chooseFuturePublishTime") });
249
+ return;
250
+ }
251
+ scheduling = true;
252
+ try {
253
+ const revision =
254
+ typeof currentDocument?._revision === "number" ? currentDocument._revision : 0;
255
+ await runtime.client.schedulePublish(slug, documentID, runAt, { revision });
256
+ schedules = await runtime.client.scheduledPublishes(slug, documentID);
257
+ scheduleAt = defaultScheduleTime();
258
+ notifications.success({
259
+ title: runtime.i18n.t("versions:publishScheduled"),
260
+ message: formatDate(runAt.toISOString()),
261
+ });
262
+ } catch (cause) {
263
+ notifications.error({
264
+ title: runtime.i18n.t("versions:publishNotScheduled"),
265
+ message:
266
+ cause instanceof Error ? cause.message : runtime.i18n.t("versions:publishScheduleFailed"),
267
+ });
268
+ } finally {
269
+ scheduling = false;
270
+ }
271
+ }
272
+
273
+ async function cancelSchedule(job: AdminScheduledPublish) {
274
+ try {
275
+ await runtime.client.cancelScheduledPublish(slug, documentID, job.id);
276
+ schedules = schedules.filter((candidate) => candidate.id !== job.id);
277
+ notifications.success({ title: runtime.i18n.t("versions:scheduledPublishCancelled") });
278
+ } catch (cause) {
279
+ notifications.error({
280
+ title: runtime.i18n.t("versions:scheduleNotCancelled"),
281
+ message:
282
+ cause instanceof Error ? cause.message : runtime.i18n.t("versions:scheduleCancelFailed"),
283
+ });
284
+ }
285
+ }
286
+
287
+ function versionPath(version: AdminVersion) {
288
+ return withContentLocale(
289
+ globalResource
290
+ ? globalVersionPath(slug, version.Revision)
291
+ : documentVersionPath(slug, documentID, version.Revision),
292
+ contentLocale
293
+ );
294
+ }
295
+
296
+ function parseRevision(value: string | undefined) {
297
+ const revision = Number(value);
298
+ return Number.isInteger(revision) && revision > 0 ? revision : undefined;
299
+ }
300
+
301
+ function defaultScheduleTime() {
302
+ const date = new Date(Date.now() + 60 * 60 * 1_000);
303
+ date.setSeconds(0, 0);
304
+ return date.toISOString();
305
+ }
306
+
307
+ function formatDate(value: string) {
308
+ const date = new Date(value);
309
+ if (Number.isNaN(date.valueOf())) return value;
310
+ return runtime.i18n.formatDate(date, { dateStyle: "medium", timeStyle: "short" });
311
+ }
312
+ </script>
313
+
314
+ <section class="mx-auto w-full max-w-6xl px-5 py-8 sm:px-8 lg:px-11">
315
+ <header class="flex flex-wrap items-start gap-4 border-b border-control-border pb-6">
316
+ <Link
317
+ class={buttonVariants({ variant: "ghost", size: "icon-sm" })}
318
+ to={editPath}
319
+ aria-label={runtime.i18n.t("versions:backToDocument")}
320
+ >
321
+ <ArrowLeftIcon class="size-3.5 rtl:rotate-180" />
322
+ </Link>
323
+ <div class="min-w-0 flex-1 basis-full sm:basis-auto">
324
+ <p class="font-mono text-[9.5px] tracking-[0.15em] text-foreground-faint uppercase">
325
+ {collection === undefined
326
+ ? slug
327
+ : runtime.i18n.text(collection.labels.singular, collection.labels.singularTranslations)} · {documentID}
328
+ </p>
329
+ <h1 class="mt-2 text-[28px] leading-tight font-semibold text-foreground">
330
+ {requestedRevision === undefined
331
+ ? runtime.i18n.t("versions:history")
332
+ : runtime.i18n.t("versions:revision", {
333
+ revision: runtime.i18n.formatNumber(requestedRevision),
334
+ })}
335
+ </h1>
336
+ <p class="mt-2 text-[13px] text-foreground-sub">
337
+ {selectedVersion === undefined
338
+ ? runtime.i18n.t("versions:historyDescription")
339
+ : runtime.i18n.t("versions:saved", {
340
+ date: formatDate(selectedVersion.CreatedAt),
341
+ status: selectedVersion.Status,
342
+ })}
343
+ </p>
344
+ </div>
345
+ {#if selectedVersion !== undefined}
346
+ <Link class={buttonVariants({ variant: "outline" })} to={historyPath}>
347
+ {runtime.i18n.t("versions:allVersions")}
348
+ </Link>
349
+ {#if operations?.update === true}
350
+ <Button disabled={restoring} onclick={() => restoreSelected(false)}>
351
+ <RotateCcwIcon class="size-3.5" />
352
+ {restoring ? runtime.i18n.t("versions:restoring") : runtime.i18n.t("versions:restore")}
353
+ </Button>
354
+ {#if collection?.versionSettings?.drafts === true}
355
+ <Button variant="outline" disabled={restoring} onclick={() => restoreSelected(true)}>
356
+ <RotateCcwIcon class="size-3.5" />
357
+ {runtime.i18n.t("versions:restoreAsDraft")}
358
+ </Button>
359
+ {/if}
360
+ {/if}
361
+ {/if}
362
+ </header>
363
+
364
+ {#if error !== undefined}
365
+ <Banner class="mt-6" tone="destructive">{error}</Banner>
366
+ {/if}
367
+
368
+ {#if loading}
369
+ <div class="mt-7 grid gap-4"><Skeleton class="h-12" /><Skeleton class="h-72" /></div>
370
+ {:else if collection === undefined}
371
+ <Banner class="mt-6" tone="warning">{runtime.i18n.t("versions:resourceUnavailable")}</Banner>
372
+ {:else if selectedVersion !== undefined}
373
+ <div
374
+ class="mt-7 flex flex-wrap items-end gap-4 rounded-[4px] border border-control-border bg-control p-4"
375
+ >
376
+ <label class="grid min-w-56 gap-1.5 text-[11.5px] text-foreground-sub">
377
+ {runtime.i18n.t("versions:compareRevisionWith", {
378
+ revision: runtime.i18n.formatNumber(selectedVersion.Revision),
379
+ })}
380
+ <Select
381
+ type="single"
382
+ value={compareRevision}
383
+ onValueChange={(revision) => (compareRevision = revision)}
384
+ >
385
+ <SelectTrigger
386
+ size="compact"
387
+ class="w-full"
388
+ aria-label={runtime.i18n.t("versions:comparisonRevision")}
389
+ >
390
+ {comparisonLabel}
391
+ </SelectTrigger>
392
+ <SelectContent>
393
+ <SelectItem value="" label={runtime.i18n.t("versions:noComparison")} />
394
+ {#each versions.filter((version) => version.Revision !== selectedVersion?.Revision) as version (version.ID)}
395
+ <SelectItem
396
+ value={String(version.Revision)}
397
+ label={runtime.i18n.t("versions:revisionWithStatus", {
398
+ revision: runtime.i18n.formatNumber(version.Revision),
399
+ status: version.Status,
400
+ })}
401
+ />
402
+ {/each}
403
+ </SelectContent>
404
+ </Select>
405
+ </label>
406
+ <label class="mb-2 flex items-center gap-2 text-[12px] text-foreground-muted">
407
+ <Checkbox bind:checked={modifiedOnly} />
408
+ {runtime.i18n.t("versions:modifiedFieldsOnly")}
409
+ </label>
410
+ </div>
411
+
412
+ <div class="mt-5 overflow-hidden rounded-[4px] border border-control-border">
413
+ <div
414
+ class="grid grid-cols-[minmax(130px,0.6fr)_minmax(0,1fr)_minmax(0,1fr)] border-b border-control-border bg-control px-4 py-2.5 font-mono text-[9.5px] tracking-[0.08em] text-foreground-faint uppercase"
415
+ >
416
+ <span>{runtime.i18n.t("versions:field")}</span>
417
+ <span>
418
+ {runtime.i18n.t("versions:revision", {
419
+ revision:
420
+ comparisonVersion === undefined
421
+ ? "—"
422
+ : runtime.i18n.formatNumber(comparisonVersion.Revision),
423
+ })}
424
+ </span>
425
+ <span>
426
+ {runtime.i18n.t("versions:revision", {
427
+ revision: runtime.i18n.formatNumber(selectedVersion.Revision),
428
+ })}
429
+ </span>
430
+ </div>
431
+ {#if visibleRows.length === 0}
432
+ <p class="px-4 py-10 text-center text-[13px] text-foreground-faint">
433
+ {runtime.i18n.t("versions:noModifiedFields")}
434
+ </p>
435
+ {:else}
436
+ {#each visibleRows as row (row.path)}
437
+ <div
438
+ class={[
439
+ "grid grid-cols-[minmax(130px,0.6fr)_minmax(0,1fr)_minmax(0,1fr)] border-b border-control-border px-4 py-3 last:border-b-0",
440
+ row.changed && "bg-primary/[0.025]",
441
+ ]}
442
+ >
443
+ <div class="pe-4">
444
+ <p class="text-[12.5px] text-foreground-muted">{row.label}</p>
445
+ <p class="font-mono mt-1 text-[9px] text-foreground-faint">{row.path}</p>
446
+ </div>
447
+ <pre
448
+ class="font-mono overflow-x-auto pe-5 text-wrap text-[11px] leading-5 text-foreground-sub">{formatVersionValue(
449
+ row.before,
450
+ runtime.i18n
451
+ )}</pre>
452
+ <pre
453
+ class="font-mono overflow-x-auto text-wrap text-[11px] leading-5 text-foreground-body">{formatVersionValue(
454
+ row.after,
455
+ runtime.i18n
456
+ )}</pre>
457
+ </div>
458
+ {/each}
459
+ {/if}
460
+ </div>
461
+ {:else}
462
+ {#if !globalResource && operations?.publish === true}
463
+ <section
464
+ class="mt-7 rounded-[4px] border border-control-border bg-control p-5"
465
+ aria-labelledby="scheduled-publishing"
466
+ >
467
+ <div class="flex items-start gap-3">
468
+ <CalendarClockIcon class="mt-0.5 size-4 text-primary" />
469
+ <div>
470
+ <h2 id="scheduled-publishing" class="text-[14px] font-medium text-foreground">
471
+ {runtime.i18n.t("versions:scheduledPublishing")}
472
+ </h2>
473
+ <p class="mt-1 text-[12px] text-foreground-sub">
474
+ {runtime.i18n.t("versions:scheduledPublishingDescription")}
475
+ </p>
476
+ </div>
477
+ </div>
478
+ <div class="mt-4 flex flex-wrap items-end gap-3">
479
+ <label class="grid min-w-64 gap-1.5 text-[11.5px] text-foreground-sub">
480
+ {runtime.i18n.t("versions:publishDateTime")}
481
+ <DateValueControl
482
+ id="scheduled-publish-at"
483
+ appearance="dayAndTime"
484
+ value={scheduleAt}
485
+ label={runtime.i18n.t("versions:publishDateTime")}
486
+ onValueChange={(next) => (scheduleAt = next)}
487
+ />
488
+ </label>
489
+ <Button disabled={scheduling} onclick={schedulePublish}>
490
+ {scheduling
491
+ ? runtime.i18n.t("versions:scheduling")
492
+ : runtime.i18n.t("versions:schedulePublish")}
493
+ </Button>
494
+ </div>
495
+ {#if schedules.length > 0}
496
+ <ul class="mt-4 divide-y divide-control-border border-t border-control-border">
497
+ {#each schedules as job (job.id)}
498
+ <li class="flex items-center gap-3 py-3 text-[12px]">
499
+ <span class="min-w-0 flex-1 text-foreground-muted">
500
+ {formatDate(job.runAt)}
501
+ <span class="font-mono ms-2 text-[9.5px] text-foreground-faint">
502
+ {runtime.i18n.t("versions:revision", {
503
+ revision: runtime.i18n.formatNumber(job.expectedRevision),
504
+ })}
505
+ </span>
506
+ </span>
507
+ <Button
508
+ size="icon-xs"
509
+ variant="ghost"
510
+ onclick={() => cancelSchedule(job)}
511
+ aria-label={runtime.i18n.t("versions:cancelScheduledPublish")}
512
+ >
513
+ <XIcon class="size-3" />
514
+ </Button>
515
+ </li>
516
+ {/each}
517
+ </ul>
518
+ {/if}
519
+ </section>
520
+ {/if}
521
+
522
+ <section class="mt-7" aria-labelledby="revision-list">
523
+ <div class="flex items-end justify-between gap-4">
524
+ <div>
525
+ <h2 id="revision-list" class="text-[14px] font-medium text-foreground">
526
+ {runtime.i18n.t("versions:savedRevisions")}
527
+ </h2>
528
+ <p class="mt-1 text-[12px] text-foreground-sub">
529
+ {runtime.i18n.t("versions:savedRevisionsDescription")}
530
+ </p>
531
+ </div>
532
+ <span class="font-mono text-[10px] text-foreground-faint">
533
+ {runtime.i18n.t("versions:total", {
534
+ count: runtime.i18n.formatNumber(versions.length),
535
+ })}
536
+ </span>
537
+ </div>
538
+ <div class="mt-4 overflow-hidden rounded-[4px] border border-control-border">
539
+ {#if versions.length === 0}
540
+ <p class="px-5 py-12 text-center text-[13px] text-foreground-faint">
541
+ {runtime.i18n.t("versions:noRevisions")}
542
+ </p>
543
+ {:else}
544
+ {#each pageVersions as version (version.ID)}
545
+ <Link
546
+ class="grid grid-cols-[70px_minmax(0,1fr)_120px] items-center border-b border-control-border px-4 py-3 transition-colors last:border-b-0 hover:bg-control"
547
+ to={versionPath(version)}
548
+ >
549
+ <span class="font-mono text-[11px] text-foreground-muted">
550
+ {runtime.i18n.t("versions:shortRevision", {
551
+ revision: runtime.i18n.formatNumber(version.Revision),
552
+ })}
553
+ </span>
554
+ <span class="text-[12.5px] text-foreground-body">
555
+ {formatDate(version.CreatedAt)}
556
+ </span>
557
+ <span
558
+ class="justify-self-end rounded-[3px] border border-control-border bg-control px-2 py-0.5 text-[10px] capitalize text-foreground-sub"
559
+ >
560
+ {version.Status}
561
+ </span>
562
+ </Link>
563
+ {/each}
564
+ {/if}
565
+ </div>
566
+ {#if versions.length > 0}
567
+ <Pagination
568
+ start={(page - 1) * pageSize + 1}
569
+ end={Math.min(page * pageSize, versions.length)}
570
+ total={versions.length}
571
+ {page}
572
+ {totalPages}
573
+ hasPrevious={page > 1}
574
+ hasNext={page < totalPages}
575
+ onPrevious={() => (page -= 1)}
576
+ onNext={() => (page += 1)}
577
+ />
578
+ {/if}
579
+ </section>
580
+ {/if}
581
+ </section>