@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,951 @@
1
+ <script lang="ts">
2
+ import { Link, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
3
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
4
+ import FolderIcon from "~icons/lucide/folder";
5
+ import RotateCcwIcon from "~icons/lucide/rotate-ccw";
6
+ import Trash2Icon from "~icons/lucide/trash-2";
7
+ import UploadCloudIcon from "~icons/lucide/upload-cloud";
8
+
9
+ import { Banner } from "@admin/components/ui/banner";
10
+ import { Button, buttonVariants } from "@admin/components/ui/button";
11
+ import { ConfirmationDialog } from "@admin/components/ui/confirmation-dialog";
12
+ import {
13
+ collectionPath,
14
+ collectionTrashPath,
15
+ collectionUploadPath,
16
+ createDocumentPath,
17
+ documentPath,
18
+ } from "@admin/core/routing/admin-paths";
19
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
20
+ import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
21
+ import type { AdminDocument } from "@admin/core/api/admin-client";
22
+ import { preferenceOwnerID } from "@admin/core/preferences/preference-write-queue";
23
+
24
+ import { CollectionListController } from "@admin/features/collections/collection-list-controller.svelte";
25
+ import CollectionListBulkActions from "@admin/features/collections/collection-list-bulk-actions.svelte";
26
+ import CollectionListBulkEditor from "@admin/features/collections/collection-list-bulk-editor.svelte";
27
+ import {
28
+ CollectionListPreferencesController,
29
+ type CollectionListPreset,
30
+ } from "@admin/features/collections/collection-list-preferences-controller.svelte";
31
+ import CollectionListResults from "@admin/features/collections/collection-list-results.svelte";
32
+ import CollectionListWorkspaceToolbar from "@admin/features/collections/collection-list-workspace-toolbar.svelte";
33
+ import {
34
+ buildListFilterWhere,
35
+ defaultListColumns,
36
+ filterableFields,
37
+ listColumnFields,
38
+ listPageSizes,
39
+ parseListFilters,
40
+ parseListPageSize,
41
+ visibleColumnNames,
42
+ type ListFilter,
43
+ type ListPageSize,
44
+ } from "@admin/features/collections/list-workspace";
45
+ import { formatDateDisplay } from "@admin/fields/scalar/date-value";
46
+
47
+ const runtime = getAdminRuntime();
48
+ const notifications = getNotificationCenter();
49
+ const navigate = useNavigate();
50
+ const params = $derived(useParams<"collection">());
51
+ const location = $derived(useLocation());
52
+ const slug = $derived(params.collection ?? "");
53
+ const trashOnly = $derived(location.pathname.endsWith("/trash"));
54
+ const collection = $derived(runtime.manifest?.collections.find((item) => item.slug === slug));
55
+ const titleField = $derived(
56
+ collection?.fields.find((field) => field.name === collection.admin.useAsTitle) ??
57
+ collection?.fields.find((field) => field.type === "text") ??
58
+ collection?.fields.find((field) => field.type === "select")
59
+ );
60
+ const searchLabel = $derived(
61
+ titleField === undefined
62
+ ? runtime.i18n.t("collections:searchDocuments")
63
+ : runtime.i18n.t("collections:searchBy", { label: titleField.admin.label })
64
+ );
65
+ const statusField = $derived(
66
+ collection?.fields.find((field) => field.type === "select" && field.name === "status")
67
+ );
68
+ const statusChoices = $derived(statusField?.select?.choices ?? []);
69
+ const searchParams = $derived(new URLSearchParams(location.search));
70
+ const localization = $derived(runtime.manifest?.application.localization);
71
+ const requestedLocale = $derived(searchParams.get("locale"));
72
+ const contentLocale = $derived(
73
+ localization?.locales.some((locale) => locale.code === requestedLocale)
74
+ ? (requestedLocale ?? localization?.defaultLocale)
75
+ : (runtime.contentLocale ?? localization?.defaultLocale)
76
+ );
77
+ const query = $derived(searchParams.get("q") ?? "");
78
+ const requestedPage = $derived(parsePage(searchParams.get("page")));
79
+ const requestedStatus = $derived(searchParams.get("status") ?? "");
80
+ const requestedFolder = $derived(searchParams.get("folder") ?? "");
81
+ const hierarchy = $derived(searchParams.get("view") === "hierarchy");
82
+ const requestedSort = $derived(searchParams.get("sort") ?? "");
83
+ const folderField = $derived(
84
+ collection?.fields.find((field) => field.name === collection.admin.folderField)
85
+ );
86
+ const parentField = $derived(
87
+ collection?.fields.find((field) => field.name === collection.admin.parentField)
88
+ );
89
+ const folderCollection = $derived(
90
+ runtime.manifest?.collections.find(
91
+ (candidate) => candidate.slug === folderField?.relationship?.collectionSlug
92
+ )
93
+ );
94
+ let folders = $state.raw<AdminDocument[]>([]);
95
+ let referenceLabels = $state.raw<Record<string, string>>({});
96
+ let searchInput = $state<HTMLInputElement | null>(null);
97
+ const activeStatus = $derived(
98
+ statusChoices.some((choice) => choice.value === requestedStatus) ? requestedStatus : ""
99
+ );
100
+ const bulkEditableFields = $derived(
101
+ collection?.fields.filter(
102
+ (field) =>
103
+ field.category === "scalar" &&
104
+ field.localized !== true &&
105
+ field.admin.readOnly !== true &&
106
+ ["text", "textarea", "email", "date", "number", "checkbox", "select", "radio"].includes(
107
+ field.type
108
+ )
109
+ ) ?? []
110
+ );
111
+ const customListCells = $derived(
112
+ runtime.listCells.flatMap((cell) => {
113
+ if (cell.collection !== slug) return [];
114
+ const field = collection?.fields.find(
115
+ (candidate) => candidate.path === cell.field || candidate.name === cell.field
116
+ );
117
+ return field === undefined ? [] : [{ ...cell, field }];
118
+ })
119
+ );
120
+ const availableColumnFields = $derived(
121
+ listColumnFields(collection, titleField?.name).filter(
122
+ (field) =>
123
+ field.path !== statusField?.path &&
124
+ !customListCells.some((cell) => cell.field.path === field.path)
125
+ )
126
+ );
127
+ const availableFilterFields = $derived(filterableFields(collection?.fields ?? []));
128
+ const defaultColumns = $derived(defaultListColumns(collection, availableColumnFields));
129
+ const preferences = new CollectionListPreferencesController(
130
+ runtime.client,
131
+ notifications,
132
+ () => runtime.session?.id ?? "",
133
+ runtime.i18n
134
+ );
135
+ const { workspace, presets, presetsPending } = $derived(preferences);
136
+ const visibleColumns = $derived(
137
+ visibleColumnNames(searchParams.get("columns"), workspace.columns).flatMap((name) => {
138
+ const field = availableColumnFields.find((candidate) => candidate.path === name);
139
+ return field === undefined ? [] : [field];
140
+ })
141
+ );
142
+ const pageSize = $derived(parseListPageSize(searchParams.get("limit"), workspace.limit));
143
+ const showID = $derived(workspace.showID);
144
+ const showCreated = $derived(workspace.showCreated);
145
+ const listFilters = $derived(
146
+ parseListFilters(searchParams.get("filters"), availableFilterFields)
147
+ );
148
+ const filterWhere = $derived(buildListFilterWhere(listFilters, availableFilterFields));
149
+ const sortField = $derived(requestedSort.replace(/^-/, ""));
150
+ const sortDescending = $derived(requestedSort.startsWith("-"));
151
+ let bulkEditOpen = $state(false);
152
+ type ConfirmationAction =
153
+ | { kind: "delete-document"; id: string }
154
+ | { kind: "delete-selected" }
155
+ | { kind: "restore-selected" }
156
+ | { kind: "delete-selected-permanently" }
157
+ | { kind: "empty-trash" };
158
+ let confirmationOpen = $state(false);
159
+ let confirmationAction = $state.raw<ConfirmationAction>();
160
+ const confirmationCopy = $derived(confirmationText(confirmationAction));
161
+ const controller = new CollectionListController({
162
+ client: runtime.client,
163
+ notifications,
164
+ i18n: runtime.i18n,
165
+ get slug() {
166
+ return slug;
167
+ },
168
+ get locale() {
169
+ return contentLocale;
170
+ },
171
+ get page() {
172
+ return requestedPage;
173
+ },
174
+ get search() {
175
+ return query;
176
+ },
177
+ get titleField() {
178
+ return titleField?.name;
179
+ },
180
+ get status() {
181
+ return activeStatus;
182
+ },
183
+ get statusField() {
184
+ return statusField?.name;
185
+ },
186
+ get statusChoices() {
187
+ return statusChoices;
188
+ },
189
+ get versioned() {
190
+ return collection?.capabilities.versions === true;
191
+ },
192
+ get trashOnly() {
193
+ return trashOnly;
194
+ },
195
+ get folderField() {
196
+ return folderField?.name;
197
+ },
198
+ get folderID() {
199
+ return requestedFolder;
200
+ },
201
+ get hierarchy() {
202
+ return hierarchy;
203
+ },
204
+ get limit() {
205
+ return pageSize;
206
+ },
207
+ get sort() {
208
+ return requestedSort === "" ? [] : [requestedSort];
209
+ },
210
+ get filterWhere() {
211
+ return filterWhere;
212
+ },
213
+ });
214
+ const {
215
+ allDocuments,
216
+ canCreate,
217
+ docs,
218
+ error,
219
+ pagination,
220
+ selectedIDs,
221
+ bulkPending,
222
+ showStatus,
223
+ showUpdated,
224
+ statusColumnAvailable,
225
+ } = $derived(controller);
226
+ const {
227
+ choiceCount,
228
+ canReadField,
229
+ deletePermanent,
230
+ bulkDelete,
231
+ bulkDeletePermanent,
232
+ bulkRestore,
233
+ emptyTrash,
234
+ bulkUpdate,
235
+ retry,
236
+ setShowStatus,
237
+ setShowUpdated,
238
+ title,
239
+ } = controller;
240
+
241
+ $effect(() => {
242
+ if (contentLocale === undefined || requestedLocale === contentLocale) return;
243
+ const next = new URLSearchParams(location.search);
244
+ next.set("locale", contentLocale);
245
+ updateSearch(next, true);
246
+ });
247
+
248
+ $effect(() =>
249
+ runtime.registerContentLocaleBlocker(
250
+ () =>
251
+ controller.bulkPending || controller.selectionPending || controller.mutationPending.size > 0
252
+ )
253
+ );
254
+
255
+ $effect(() => {
256
+ preferences.enter({
257
+ slug,
258
+ sessionID: runtime.session?.id ?? "",
259
+ preferenceOwnerID: preferenceOwnerID(runtime.session),
260
+ columnFields: availableColumnFields,
261
+ filterFields: availableFilterFields,
262
+ defaultColumns,
263
+ });
264
+ });
265
+
266
+ $effect(() => {
267
+ setShowStatus(workspace.showStatus);
268
+ setShowUpdated(workspace.showUpdated);
269
+ });
270
+
271
+ $effect(() => () => preferences.dispose());
272
+
273
+ $effect(() => {
274
+ const target = folderCollection;
275
+ const revision = runtime.documentRevision;
276
+ if (target === undefined || revision < 0) {
277
+ folders = [];
278
+ return;
279
+ }
280
+ const request = new AbortController();
281
+ runtime.client
282
+ .list(target.slug, { limit: 100, locale: contentLocale, signal: request.signal })
283
+ .then((page) => {
284
+ if (!request.signal.aborted) folders = page.docs;
285
+ })
286
+ .catch(() => {
287
+ if (!request.signal.aborted) folders = [];
288
+ });
289
+ return () => request.abort();
290
+ });
291
+
292
+ $effect(() => {
293
+ const documents = docs;
294
+ const fields = visibleColumns.filter(referenceField);
295
+ const collections = runtime.manifest?.collections ?? [];
296
+ if (documents.length === 0 || fields.length === 0) {
297
+ referenceLabels = {};
298
+ return;
299
+ }
300
+ const lookups = new Map<string, { collection: SchemaCollection; id: string }>();
301
+ for (const document of documents) {
302
+ for (const field of fields) {
303
+ for (const reference of fieldReferences(field, documentPathValue(document, field.path))) {
304
+ const target = collections.find((candidate) => candidate.slug === reference.collection);
305
+ if (target !== undefined) {
306
+ lookups.set(referenceKey(reference.collection, reference.id), {
307
+ collection: target,
308
+ id: reference.id,
309
+ });
310
+ }
311
+ }
312
+ }
313
+ }
314
+ const request = new AbortController();
315
+ Promise.all(
316
+ [...lookups].map(async ([key, lookup]) => {
317
+ try {
318
+ const document = await runtime.client.find(lookup.collection.slug, lookup.id, {
319
+ signal: request.signal,
320
+ locale: contentLocale,
321
+ });
322
+ return [key, referenceTitle(lookup.collection, document)] as const;
323
+ } catch {
324
+ return [key, lookup.id] as const;
325
+ }
326
+ })
327
+ ).then((entries) => {
328
+ if (!request.signal.aborted) referenceLabels = Object.fromEntries(entries);
329
+ });
330
+ return () => request.abort();
331
+ });
332
+
333
+ function parsePage(value: string | null) {
334
+ const page = Number(value);
335
+ return Number.isInteger(page) && page > 0 ? page : 1;
336
+ }
337
+
338
+ function contentPath(path: string) {
339
+ if (contentLocale === undefined) return path;
340
+ return `${path}?${new URLSearchParams({ locale: contentLocale }).toString()}`;
341
+ }
342
+
343
+ function updateSearch(next: URLSearchParams, replace: boolean) {
344
+ navigate(
345
+ { pathname: location.pathname, search: next.size === 0 ? "" : `?${next}` },
346
+ { replace }
347
+ );
348
+ }
349
+
350
+ function handleSearch(value: string) {
351
+ const next = new URLSearchParams(searchParams);
352
+ if (value.trim().length > 0) next.set("q", value);
353
+ else next.delete("q");
354
+ next.delete("page");
355
+ updateSearch(next, true);
356
+ }
357
+
358
+ function clearSearch() {
359
+ const next = new URLSearchParams(searchParams);
360
+ next.delete("q");
361
+ next.delete("page");
362
+ updateSearch(next, true);
363
+ }
364
+
365
+ function clearFilters() {
366
+ const next = new URLSearchParams(searchParams);
367
+ next.delete("q");
368
+ next.delete("status");
369
+ next.delete("page");
370
+ next.delete("folder");
371
+ next.delete("filters");
372
+ updateSearch(next, true);
373
+ }
374
+
375
+ function clearFiltersAndFocusSearch() {
376
+ clearFilters();
377
+ searchInput?.focus();
378
+ }
379
+
380
+ function updateListFilters(filters: readonly ListFilter[]) {
381
+ const next = new URLSearchParams(searchParams);
382
+ if (filters.length === 0) next.delete("filters");
383
+ else next.set("filters", JSON.stringify(filters));
384
+ next.delete("page");
385
+ updateSearch(next, false);
386
+ }
387
+
388
+ function toggleSort(name: string) {
389
+ const next = new URLSearchParams(searchParams);
390
+ if (sortField !== name) next.set("sort", name);
391
+ else if (!sortDescending) next.set("sort", `-${name}`);
392
+ else next.delete("sort");
393
+ next.delete("page");
394
+ updateSearch(next, false);
395
+ }
396
+
397
+ function selectPageSize(limit: ListPageSize) {
398
+ const next = new URLSearchParams(searchParams);
399
+ next.set("limit", String(limit));
400
+ next.delete("page");
401
+ updateSearch(next, false);
402
+ void preferences.persistWorkspace({ ...workspace, limit });
403
+ }
404
+
405
+ function toggleColumn(name: string, show: boolean) {
406
+ // Workspace state is updated synchronously by persistWorkspace. Using it as
407
+ // the source prevents two quick checkbox changes from racing a router update.
408
+ const nextNames = new Set(workspace.columns);
409
+ if (show) nextNames.add(name);
410
+ else nextNames.delete(name);
411
+ const columns = availableColumnFields
412
+ .map((field) => field.path)
413
+ .filter((candidate) => nextNames.has(candidate));
414
+ const next = new URLSearchParams(searchParams);
415
+ if (columns.length === 0) next.set("columns", "");
416
+ else next.set("columns", columns.join(","));
417
+ updateSearch(next, true);
418
+ void preferences.persistWorkspace({ ...workspace, columns });
419
+ }
420
+
421
+ function toggleStatusColumn(show: boolean) {
422
+ void preferences.persistWorkspace({ ...workspace, showStatus: show });
423
+ }
424
+
425
+ function toggleUpdatedColumn(show: boolean) {
426
+ void preferences.persistWorkspace({ ...workspace, showUpdated: show });
427
+ }
428
+
429
+ function toggleIDColumn(show: boolean) {
430
+ void preferences.persistWorkspace({ ...workspace, showID: show });
431
+ }
432
+
433
+ function toggleCreatedColumn(show: boolean) {
434
+ void preferences.persistWorkspace({ ...workspace, showCreated: show });
435
+ }
436
+
437
+ function selectFolder(folder: string) {
438
+ const next = new URLSearchParams(searchParams);
439
+ if (folder === "") next.delete("folder");
440
+ else next.set("folder", folder);
441
+ next.delete("page");
442
+ updateSearch(next, false);
443
+ }
444
+
445
+ function selectView(view: "list" | "hierarchy") {
446
+ const next = new URLSearchParams(searchParams);
447
+ if (view === "list") next.delete("view");
448
+ else next.set("view", view);
449
+ next.delete("page");
450
+ updateSearch(next, false);
451
+ }
452
+
453
+ function savePreset(name: string) {
454
+ return preferences.savePreset(name, {
455
+ q: query,
456
+ status: activeStatus,
457
+ folder: requestedFolder,
458
+ view: hierarchy ? "hierarchy" : "list",
459
+ filters: listFilters,
460
+ sort: requestedSort,
461
+ columns: visibleColumns.map((field) => field.path),
462
+ limit: pageSize,
463
+ showStatus,
464
+ showID,
465
+ showCreated,
466
+ showUpdated,
467
+ });
468
+ }
469
+
470
+ function applyPreset(preset: CollectionListPreset) {
471
+ const next = new URLSearchParams();
472
+ if (contentLocale !== undefined) next.set("locale", contentLocale);
473
+ if (preset.q !== "") next.set("q", preset.q);
474
+ if (preset.status !== "") next.set("status", preset.status);
475
+ if (preset.folder !== "") next.set("folder", preset.folder);
476
+ if (preset.view === "hierarchy") next.set("view", "hierarchy");
477
+ if (preset.filters.length > 0) next.set("filters", JSON.stringify(preset.filters));
478
+ if (preset.sort !== "") next.set("sort", preset.sort);
479
+ if (preset.columns.length > 0) next.set("columns", preset.columns.join(","));
480
+ next.set("limit", String(preset.limit));
481
+ void preferences.persistWorkspace({
482
+ columns: preset.columns,
483
+ limit: preset.limit,
484
+ showStatus: preset.showStatus,
485
+ showID: preset.showID,
486
+ showCreated: preset.showCreated,
487
+ showUpdated: preset.showUpdated,
488
+ });
489
+ updateSearch(next, false);
490
+ }
491
+
492
+ function selectStatus(status: string) {
493
+ const next = new URLSearchParams(searchParams);
494
+ if (status === "") next.delete("status");
495
+ else next.set("status", status);
496
+ next.delete("page");
497
+ updateSearch(next, false);
498
+ }
499
+
500
+ function selectPage(page: number) {
501
+ const next = new URLSearchParams(searchParams);
502
+ if (page <= 1) next.delete("page");
503
+ else next.set("page", String(page));
504
+ updateSearch(next, false);
505
+ }
506
+
507
+ function columnValue(document: AdminDocument, field: SchemaField) {
508
+ if (!canReadField(field.path, document.id)) return "—";
509
+ const value = documentPathValue(document, field.path);
510
+ if (value === undefined || value === null || value === "") return "—";
511
+ if (field.type === "checkbox") {
512
+ return value === true ? runtime.i18n.t("general:yes") : runtime.i18n.t("general:no");
513
+ }
514
+ if (field.type === "select" || field.type === "radio") {
515
+ return field.select?.choices.find((choice) => choice.value === value)?.label ?? String(value);
516
+ }
517
+ if (field.type === "array") return summarizeRows(value, "row");
518
+ if (field.type === "blocks") return summarizeRows(value, "block");
519
+ if (field.type === "json" || field.plugin !== undefined) return summarizeStructured(value);
520
+ if (field.type === "date")
521
+ return formatDateDisplay(value, field.date?.pickerAppearance, runtime.i18n);
522
+ if (referenceField(field)) {
523
+ return runtime.i18n.formatList(
524
+ fieldReferences(field, value).map(
525
+ (reference) =>
526
+ referenceLabels[referenceKey(reference.collection, reference.id)] ?? reference.id
527
+ )
528
+ );
529
+ }
530
+ if (Array.isArray(value)) return runtime.i18n.formatList(value.map(displayReference));
531
+ if (typeof value === "object") return displayReference(value);
532
+ return String(value);
533
+ }
534
+
535
+ function referenceField(field: SchemaField) {
536
+ return (
537
+ field.relationship !== undefined || field.upload !== undefined || field.join !== undefined
538
+ );
539
+ }
540
+
541
+ function fieldReferences(field: SchemaField, value: unknown) {
542
+ const values = Array.isArray(value) ? value : [value];
543
+ return values.flatMap((candidate): { collection: string; id: string }[] => {
544
+ if (typeof candidate === "string") {
545
+ const target =
546
+ field.relationship?.collectionSlug ??
547
+ field.upload?.collectionSlug ??
548
+ field.join?.collectionSlug;
549
+ return target === undefined ? [] : [{ collection: target, id: candidate }];
550
+ }
551
+ if (typeof candidate !== "object" || candidate === null) return [];
552
+ const record = candidate as Record<string, unknown>;
553
+ const id = typeof record.id === "string" ? record.id : undefined;
554
+ const target =
555
+ typeof record.relationTo === "string"
556
+ ? record.relationTo
557
+ : (field.relationship?.collectionSlug ??
558
+ field.upload?.collectionSlug ??
559
+ field.join?.collectionSlug);
560
+ return id === undefined || target === undefined ? [] : [{ collection: target, id }];
561
+ });
562
+ }
563
+
564
+ function referenceKey(collection: string, id: string) {
565
+ return `${collection}:${id}`;
566
+ }
567
+
568
+ function referenceTitle(collection: SchemaCollection, document: AdminDocument) {
569
+ const titleField =
570
+ collection.fields.find((field) => field.name === collection.admin.useAsTitle) ??
571
+ collection.fields.find((field) => ["text", "email", "select", "radio"].includes(field.type));
572
+ const value =
573
+ titleField === undefined ? undefined : documentPathValue(document, titleField.path);
574
+ return value === undefined || value === null || value === "" ? document.id : String(value);
575
+ }
576
+
577
+ function displayReference(value: unknown) {
578
+ if (typeof value === "string") return value;
579
+ if (typeof value !== "object" || value === null) return String(value ?? "—");
580
+ const record = value as Record<string, unknown>;
581
+ return String(
582
+ record.title ??
583
+ record.name ??
584
+ record.email ??
585
+ record.id ??
586
+ runtime.i18n.t("collections:relatedDocument")
587
+ );
588
+ }
589
+
590
+ function documentPathValue(document: AdminDocument, path: string) {
591
+ let value: unknown = document;
592
+ for (const segment of path.split(".")) {
593
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
594
+ value = Reflect.get(value, segment);
595
+ }
596
+ return value;
597
+ }
598
+
599
+ function summarizeRows(value: unknown, singular: "row" | "block") {
600
+ if (!Array.isArray(value) || value.length === 0) return "—";
601
+ const previews = value.slice(0, 2).map((row) => {
602
+ if (typeof row !== "object" || row === null) return String(row);
603
+ const record = row as Record<string, unknown>;
604
+ return String(
605
+ record.label ??
606
+ record.title ??
607
+ record.heading ??
608
+ record.name ??
609
+ record.blockType ??
610
+ runtime.i18n.t(singular === "row" ? "collections:rowNumber" : "collections:blockNumber", {
611
+ number: runtime.i18n.formatNumber(value.indexOf(row) + 1),
612
+ })
613
+ );
614
+ });
615
+ const summary = runtime.i18n.formatList(previews);
616
+ return value.length > previews.length
617
+ ? runtime.i18n.t("collections:moreItems", {
618
+ summary,
619
+ count: runtime.i18n.formatNumber(value.length - previews.length),
620
+ })
621
+ : summary;
622
+ }
623
+
624
+ function summarizeStructured(value: unknown) {
625
+ if (value === undefined || value === null) return "—";
626
+ if (typeof value !== "object") return String(value);
627
+ const text = JSON.stringify(value);
628
+ return text.length > 72 ? `${text.slice(0, 69)}…` : text;
629
+ }
630
+
631
+ function requestConfirmation(action: ConfirmationAction) {
632
+ confirmationAction = action;
633
+ confirmationOpen = true;
634
+ }
635
+
636
+ function openBulkEdit() {
637
+ bulkEditOpen = true;
638
+ }
639
+
640
+ function applyBulkEdit(field: SchemaField, value: unknown) {
641
+ return bulkUpdate({ [field.name]: value });
642
+ }
643
+
644
+ async function performConfirmation() {
645
+ const action = confirmationAction;
646
+ if (action === undefined) return;
647
+ if (action.kind === "delete-document") await deletePermanent(action.id);
648
+ else if (action.kind === "delete-selected") await bulkDelete();
649
+ else if (action.kind === "restore-selected") await bulkRestore();
650
+ else if (action.kind === "delete-selected-permanently") await bulkDeletePermanent();
651
+ else await emptyTrash();
652
+ }
653
+
654
+ function confirmationText(action: ConfirmationAction | undefined) {
655
+ const count = selectedIDs.size;
656
+ if (action?.kind === "delete-document") {
657
+ return {
658
+ title: runtime.i18n.t("collections:confirmDeleteDocumentPermanently"),
659
+ description: runtime.i18n.t("collections:cannotUndo"),
660
+ confirmLabel: runtime.i18n.t("collections:deletePermanently"),
661
+ destructive: true,
662
+ };
663
+ }
664
+ if (action?.kind === "restore-selected") {
665
+ return {
666
+ title: runtime.i18n.t("collections:confirmRestoreSelected", {
667
+ count,
668
+ formattedCount: runtime.i18n.formatNumber(count),
669
+ }),
670
+ description: runtime.i18n.t("collections:restoreSelectedDescription"),
671
+ confirmLabel: runtime.i18n.t("documents:restore"),
672
+ destructive: false,
673
+ };
674
+ }
675
+ if (action?.kind === "delete-selected-permanently") {
676
+ return {
677
+ title: runtime.i18n.t("collections:confirmDeleteSelectedPermanently", {
678
+ count,
679
+ formattedCount: runtime.i18n.formatNumber(count),
680
+ }),
681
+ description: runtime.i18n.t("collections:cannotUndo"),
682
+ confirmLabel: runtime.i18n.t("collections:deletePermanently"),
683
+ destructive: true,
684
+ };
685
+ }
686
+ if (action?.kind === "empty-trash") {
687
+ const trashCount = pagination.totalDocs;
688
+ return {
689
+ title: runtime.i18n.t("collections:confirmEmptyTrash", {
690
+ label:
691
+ collection?.labels.plural.toLocaleLowerCase(runtime.i18n.language) ??
692
+ runtime.i18n.t("collections:documents"),
693
+ }),
694
+ description: runtime.i18n.t("collections:emptyTrashDescription", {
695
+ count: trashCount,
696
+ formattedCount: runtime.i18n.formatNumber(trashCount),
697
+ }),
698
+ confirmLabel: runtime.i18n.t("collections:emptyTrash"),
699
+ destructive: true,
700
+ };
701
+ }
702
+ return {
703
+ title: runtime.i18n.t("collections:confirmDeleteSelected", {
704
+ count,
705
+ formattedCount: runtime.i18n.formatNumber(count),
706
+ }),
707
+ description: runtime.i18n.t("collections:deleteSelectedDescription"),
708
+ confirmLabel: runtime.i18n.t("general:delete"),
709
+ destructive: true,
710
+ };
711
+ }
712
+ </script>
713
+
714
+ <section class="w-full px-5 py-7 sm:px-8 sm:py-8 lg:px-10 lg:pb-20">
715
+ <header class="flex flex-wrap items-start justify-between gap-4">
716
+ <div>
717
+ <div class="flex flex-wrap items-center gap-3">
718
+ <h1
719
+ class="text-[28px] font-semibold leading-tight tracking-[-0.02em] text-foreground-strong"
720
+ >
721
+ {collection?.labels.plural ?? slug}
722
+ </h1>
723
+ {#if !trashOnly && canCreate}
724
+ <Link class={buttonVariants()} to={contentPath(createDocumentPath(slug))}>
725
+ {runtime.i18n.t("general:create")}
726
+ </Link>
727
+ {/if}
728
+ </div>
729
+ <p class="mt-1.5 text-[12.5px] text-foreground-muted">
730
+ {runtime.i18n.t("collections:documentCount", {
731
+ count: allDocuments,
732
+ formattedCount: runtime.i18n.formatNumber(allDocuments),
733
+ })}
734
+ {#if collection?.capabilities.versions === true}
735
+ <span aria-hidden="true">·</span>
736
+ {runtime.i18n.t("collections:draftsAndVersions")}
737
+ {/if}
738
+ </p>
739
+ </div>
740
+ <div class="flex items-center gap-2">
741
+ {#if !trashOnly && canCreate && collection?.capabilities.upload === true}
742
+ <Link
743
+ class={buttonVariants({ variant: "outline" })}
744
+ to={contentPath(collectionUploadPath(slug))}
745
+ >
746
+ <UploadCloudIcon class="size-3.5" />
747
+ {runtime.i18n.t("collections:bulkUpload")}
748
+ </Link>
749
+ {/if}
750
+ {#if collection?.capabilities.trash === true}
751
+ {#if trashOnly}
752
+ <Button
753
+ variant="destructive"
754
+ disabled={pagination.totalDocs === 0 || bulkPending}
755
+ onclick={() => requestConfirmation({ kind: "empty-trash" })}
756
+ >
757
+ {runtime.i18n.t("collections:emptyTrash")}
758
+ </Button>
759
+ {/if}
760
+ <Link
761
+ class={buttonVariants({ variant: "outline" })}
762
+ to={contentPath(trashOnly ? collectionPath(slug) : collectionTrashPath(slug))}
763
+ >
764
+ {#if trashOnly}<RotateCcwIcon class="size-3.5" />
765
+ {runtime.i18n.t("collections:backToCollection")}{:else}<Trash2Icon class="size-3.5" />
766
+ {runtime.i18n.t("collections:trash")}{/if}
767
+ </Link>
768
+ {/if}
769
+ </div>
770
+ </header>
771
+
772
+ {#if statusChoices.length > 0 && !trashOnly}
773
+ <div
774
+ class="mt-6 flex h-9 w-full items-center justify-start gap-5 border-b border-control-border"
775
+ role="group"
776
+ aria-label={runtime.i18n.t("collections:statusFilters")}
777
+ >
778
+ <button
779
+ type="button"
780
+ class="relative h-9 flex-none cursor-pointer px-px pb-2.5 text-[13px] text-foreground-muted outline-none after:absolute after:inset-x-0 after:bottom-[-1px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity hover:text-foreground-strong focus-visible:outline-2 focus-visible:outline-primary/60 focus-visible:outline-offset-2 aria-pressed:text-foreground-strong aria-pressed:after:opacity-100"
781
+ aria-pressed={activeStatus === ""}
782
+ onclick={() => selectStatus("")}
783
+ >
784
+ {runtime.i18n.t("collections:all")}
785
+ <span class="font-mono ms-1 text-[9.5px] text-foreground-faint">
786
+ {runtime.i18n.formatNumber(allDocuments)}
787
+ </span>
788
+ </button>
789
+ {#each statusChoices as choice (choice.value)}
790
+ {const statusCount = $derived(choiceCount(choice))}
791
+ <button
792
+ type="button"
793
+ class="relative h-9 flex-none cursor-pointer px-px pb-2.5 text-[13px] text-foreground-muted outline-none after:absolute after:inset-x-0 after:bottom-[-1px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity hover:text-foreground-strong focus-visible:outline-2 focus-visible:outline-primary/60 focus-visible:outline-offset-2 aria-pressed:text-foreground-strong aria-pressed:after:opacity-100"
794
+ aria-pressed={activeStatus === choice.value}
795
+ onclick={() => selectStatus(choice.value)}
796
+ >
797
+ {choice.label}
798
+ {#if statusCount !== undefined}
799
+ <span class="font-mono ms-1 text-[9.5px] text-foreground-faint">
800
+ {runtime.i18n.formatNumber(statusCount)}
801
+ </span>
802
+ {/if}
803
+ </button>
804
+ {/each}
805
+ </div>
806
+ {/if}
807
+
808
+ {#if error !== undefined}
809
+ <Banner class="mt-6" tone="destructive">
810
+ <span class="size-2 shrink-0 rounded-full bg-destructive" aria-hidden="true"></span>
811
+ <span class="min-w-0 flex-1">{error}</span>
812
+ <Button variant="outline" size="sm" onclick={retry}>
813
+ {runtime.i18n.t("general:retry")}
814
+ </Button>
815
+ </Banner>
816
+ {/if}
817
+
818
+ {#snippet manageFoldersLink()}
819
+ {#if folderCollection !== undefined}
820
+ <Link
821
+ class={buttonVariants({ variant: "outline", class: "h-[31px] gap-1.75 px-3" })}
822
+ to={contentPath(collectionPath(folderCollection.slug))}
823
+ >
824
+ <FolderIcon class="size-3" />
825
+ {runtime.i18n.t("collections:manageFolders")}
826
+ </Link>
827
+ {/if}
828
+ {/snippet}
829
+
830
+ <CollectionListWorkspaceToolbar
831
+ {slug}
832
+ {query}
833
+ {searchLabel}
834
+ bind:searchInput
835
+ filterFields={availableFilterFields}
836
+ filters={listFilters}
837
+ {canReadField}
838
+ {folders}
839
+ {requestedFolder}
840
+ manageFoldersLink={folderCollection === undefined ? undefined : manageFoldersLink}
841
+ showFolder={folderField !== undefined && !trashOnly}
842
+ showHierarchy={parentField !== undefined && !trashOnly}
843
+ {hierarchy}
844
+ sessionActive={runtime.session !== undefined && !trashOnly}
845
+ {presets}
846
+ {presetsPending}
847
+ {statusColumnAvailable}
848
+ {availableColumnFields}
849
+ {visibleColumns}
850
+ {showStatus}
851
+ {showID}
852
+ {showCreated}
853
+ {showUpdated}
854
+ {pageSize}
855
+ onSearch={handleSearch}
856
+ onClearSearch={clearSearch}
857
+ onFiltersChange={updateListFilters}
858
+ onSelectFolder={selectFolder}
859
+ onSelectView={selectView}
860
+ onSavePreset={savePreset}
861
+ onApplyPreset={applyPreset}
862
+ onDeletePreset={preferences.deletePreset}
863
+ onToggleColumn={toggleColumn}
864
+ onToggleStatus={toggleStatusColumn}
865
+ onToggleID={toggleIDColumn}
866
+ onToggleCreated={toggleCreatedColumn}
867
+ onToggleUpdated={toggleUpdatedColumn}
868
+ onSelectPageSize={selectPageSize}
869
+ />
870
+
871
+ <CollectionListResults
872
+ i18n={runtime.i18n}
873
+ {controller}
874
+ resourceKey={slug}
875
+ {collection}
876
+ {titleField}
877
+ {customListCells}
878
+ {visibleColumns}
879
+ {showID}
880
+ {showCreated}
881
+ {showUpdated}
882
+ parentFieldName={parentField?.name}
883
+ {hierarchy}
884
+ {trashOnly}
885
+ filtered={query !== "" ||
886
+ activeStatus !== "" ||
887
+ requestedFolder !== "" ||
888
+ listFilters.length > 0}
889
+ {sortField}
890
+ {sortDescending}
891
+ {columnValue}
892
+ onClearFilters={clearFiltersAndFocusSearch}
893
+ onToggleSort={toggleSort}
894
+ onSelectPage={selectPage}
895
+ onRequestDeletePermanent={(id) => requestConfirmation({ kind: "delete-document", id })}
896
+ >
897
+ {#snippet documentLink(document)}
898
+ <Link class="block max-w-[600px]" to={contentPath(documentPath(slug, document.id))}>
899
+ <span
900
+ class="block truncate text-[14px] font-medium text-foreground-strong transition-colors group-hover:text-primary"
901
+ >
902
+ {title(document)}
903
+ </span>
904
+ </Link>
905
+ {/snippet}
906
+ {#snippet createAction()}
907
+ <Link
908
+ class={buttonVariants({ variant: "outline" })}
909
+ to={contentPath(createDocumentPath(slug))}
910
+ >
911
+ {runtime.i18n.t("collections:createNew", {
912
+ label:
913
+ collection?.labels.singular.toLocaleLowerCase(runtime.i18n.language) ??
914
+ runtime.i18n.t("collections:document"),
915
+ })}
916
+ </Link>
917
+ {/snippet}
918
+ </CollectionListResults>
919
+ </section>
920
+
921
+ <CollectionListBulkActions
922
+ i18n={runtime.i18n}
923
+ {controller}
924
+ {trashOnly}
925
+ versioned={collection?.capabilities.versions === true}
926
+ hasBulkEditableFields={bulkEditableFields.length > 0}
927
+ onEdit={openBulkEdit}
928
+ onRequestDelete={() => requestConfirmation({ kind: "delete-selected" })}
929
+ onRequestRestore={() => requestConfirmation({ kind: "restore-selected" })}
930
+ onRequestDeletePermanent={() => requestConfirmation({ kind: "delete-selected-permanently" })}
931
+ onOpenDocument={(id) => navigate(contentPath(documentPath(slug, id)))}
932
+ />
933
+
934
+ <ConfirmationDialog
935
+ bind:open={confirmationOpen}
936
+ title={confirmationCopy.title}
937
+ description={confirmationCopy.description}
938
+ confirmLabel={confirmationCopy.confirmLabel}
939
+ cancelLabel={runtime.i18n.t("general:cancel")}
940
+ destructive={confirmationCopy.destructive}
941
+ disabled={bulkPending}
942
+ onconfirm={performConfirmation}
943
+ />
944
+
945
+ <CollectionListBulkEditor
946
+ bind:open={bulkEditOpen}
947
+ selectedCount={selectedIDs.size}
948
+ fields={bulkEditableFields}
949
+ pending={bulkPending}
950
+ onApply={applyBulkEdit}
951
+ />