@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.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +217 -0
- package/package.json +62 -0
- package/src/app/admin-core-view-route.svelte +89 -0
- package/src/app/admin-layout.svelte +457 -0
- package/src/app/admin-navigation-progress.svelte +204 -0
- package/src/app/admin-page-title.svelte +94 -0
- package/src/app/admin-provider-layer.svelte +26 -0
- package/src/app/admin-router-root.svelte +9 -0
- package/src/app/admin-routes.svelte +116 -0
- package/src/app/deferred-redirect.svelte +23 -0
- package/src/app/loading-boundary.svelte +70 -0
- package/src/app/not-found-route.svelte +29 -0
- package/src/app-meta.ts +2 -0
- package/src/app.css +467 -0
- package/src/app.svelte +88 -0
- package/src/assets/ridu-logo-arch.svg +3 -0
- package/src/assets/ridu-word-logo.svg +7 -0
- package/src/components/brand/ridu-logo.svelte +24 -0
- package/src/components/code-block/code-block.svelte +107 -0
- package/src/components/json-tree/json-tree-node.svelte +77 -0
- package/src/components/json-tree/json-viewer.svelte +113 -0
- package/src/components/ui/banner/banner.svelte +41 -0
- package/src/components/ui/banner/index.ts +5 -0
- package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
- package/src/components/ui/bulk-action-bar/index.ts +1 -0
- package/src/components/ui/button/index.ts +8 -0
- package/src/components/ui/calendar/calendar-caption.svelte +72 -0
- package/src/components/ui/calendar/calendar-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-day.svelte +27 -0
- package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
- package/src/components/ui/calendar/calendar-grid.svelte +17 -0
- package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-header.svelte +20 -0
- package/src/components/ui/calendar/calendar-heading.svelte +17 -0
- package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
- package/src/components/ui/calendar/calendar-month.svelte +16 -0
- package/src/components/ui/calendar/calendar-months.svelte +20 -0
- package/src/components/ui/calendar/calendar-nav.svelte +20 -0
- package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
- package/src/components/ui/calendar/calendar.svelte +118 -0
- package/src/components/ui/calendar/index.ts +1 -0
- package/src/components/ui/checkbox/checkbox.svelte +35 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
- package/src/components/ui/confirmation-dialog/index.ts +1 -0
- package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
- package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
- package/src/components/ui/date-value-control/index.ts +3 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +52 -0
- package/src/components/ui/dialog/dialog-description.svelte +20 -0
- package/src/components/ui/dialog/dialog-footer.svelte +20 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +8 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +5 -0
- package/src/components/ui/empty-state/empty-state.svelte +34 -0
- package/src/components/ui/empty-state/index.ts +1 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/kbd/index.ts +1 -0
- package/src/components/ui/kbd/kbd.svelte +24 -0
- package/src/components/ui/pagination/index.ts +1 -0
- package/src/components/ui/pagination/pagination.svelte +88 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/popover/popover-content.svelte +29 -0
- package/src/components/ui/popover/popover-trigger.svelte +11 -0
- package/src/components/ui/popover/popover.svelte +7 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
- package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
- package/src/components/ui/radio-group/radio-group.svelte +19 -0
- package/src/components/ui/select/index.ts +4 -0
- package/src/components/ui/select/select-content.svelte +38 -0
- package/src/components/ui/select/select-item.svelte +40 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/sheet/index.ts +6 -0
- package/src/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/components/ui/sheet/sheet-content.svelte +77 -0
- package/src/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
- package/src/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/components/ui/sheet/sheet.svelte +7 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/skeleton/skeleton.svelte +31 -0
- package/src/components/ui/slider/index.ts +3 -0
- package/src/components/ui/slider/slider-content.svelte +59 -0
- package/src/components/ui/slider/slider.svelte +35 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +55 -0
- package/src/components/ui/status-indicator/index.ts +5 -0
- package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
- package/src/components/ui/table/index.ts +6 -0
- package/src/components/ui/table/table-body.svelte +20 -0
- package/src/components/ui/table/table-cell.svelte +23 -0
- package/src/components/ui/table/table-head.svelte +23 -0
- package/src/components/ui/table/table-header.svelte +20 -0
- package/src/components/ui/table/table-row.svelte +23 -0
- package/src/components/ui/table/table.svelte +22 -0
- package/src/components/ui/tabs/index.ts +8 -0
- package/src/components/ui/tabs/tabs-content.svelte +17 -0
- package/src/components/ui/tabs/tabs-list.svelte +40 -0
- package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/src/components/ui/tabs/tabs.svelte +19 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
- package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/core/api/admin-client.ts +45 -0
- package/src/core/forms/field-condition.ts +63 -0
- package/src/core/forms/form-controller.svelte.ts +369 -0
- package/src/core/forms/form-draft-recovery.ts +80 -0
- package/src/core/forms/form-schema.ts +609 -0
- package/src/core/forms/form-validation.ts +750 -0
- package/src/core/i18n/admin-i18n.svelte.ts +364 -0
- package/src/core/i18n/localized-schema.ts +111 -0
- package/src/core/notifications/notification-center.svelte.ts +69 -0
- package/src/core/notifications/validation-toast.svelte +18 -0
- package/src/core/plugins/field-registry.ts +60 -0
- package/src/core/plugins/row-label-registry.ts +62 -0
- package/src/core/preferences/preference-write-queue.ts +107 -0
- package/src/core/routing/admin-paths.ts +137 -0
- package/src/core/runtime/admin-runtime.svelte.ts +625 -0
- package/src/features/auth/account-menu.svelte +159 -0
- package/src/features/auth/account-navigation.svelte +44 -0
- package/src/features/auth/account-route.svelte +395 -0
- package/src/features/auth/account-security-controller.svelte.ts +119 -0
- package/src/features/auth/account-security-route.svelte +460 -0
- package/src/features/auth/auth-brand.svelte +17 -0
- package/src/features/auth/auth-frame.svelte +18 -0
- package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
- package/src/features/auth/create-first-user-route.svelte +174 -0
- package/src/features/auth/forgot-password-route.svelte +78 -0
- package/src/features/auth/login-route.svelte +221 -0
- package/src/features/auth/request-verification-route.svelte +73 -0
- package/src/features/auth/reset-password-route.svelte +107 -0
- package/src/features/auth/verify-email-route.svelte +60 -0
- package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
- package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
- package/src/features/collections/collection-list-controller.svelte.ts +717 -0
- package/src/features/collections/collection-list-filter-popover.svelte +238 -0
- package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
- package/src/features/collections/collection-list-results.svelte +410 -0
- package/src/features/collections/collection-list-route.svelte +951 -0
- package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
- package/src/features/collections/collection-table-skeleton.svelte +44 -0
- package/src/features/collections/list-workspace.ts +265 -0
- package/src/features/dashboard/dashboard-route.svelte +141 -0
- package/src/features/development/development-tools.svelte +217 -0
- package/src/features/documents/document-api-view-loader.svelte +31 -0
- package/src/features/documents/document-api-view.svelte +547 -0
- package/src/features/documents/document-controller.svelte.ts +1252 -0
- package/src/features/documents/document-field-partition.ts +18 -0
- package/src/features/documents/document-field-sections.svelte +46 -0
- package/src/features/documents/document-heading.ts +20 -0
- package/src/features/documents/document-route-dirty.ts +43 -0
- package/src/features/documents/document-route-loader.svelte +22 -0
- package/src/features/documents/document-route.svelte +1124 -0
- package/src/features/documents/live-preview-panel.svelte +390 -0
- package/src/features/documents/live-preview.ts +61 -0
- package/src/features/documents/upload-document-preview-loader.svelte +40 -0
- package/src/features/documents/upload-document-preview.svelte +525 -0
- package/src/features/localization/content-locale-switcher.svelte +93 -0
- package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
- package/src/features/navigation/admin-command-menu.svelte +245 -0
- package/src/features/navigation/admin-resource-groups.ts +72 -0
- package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
- package/src/features/reference-browser/reference-browser.svelte +844 -0
- package/src/features/uploads/bulk-upload-route.svelte +624 -0
- package/src/features/uploads/bulk-upload.ts +39 -0
- package/src/features/uploads/upload-metadata-fields.svelte +112 -0
- package/src/features/versions/version-diff.ts +72 -0
- package/src/features/versions/version-history-route.svelte +581 -0
- package/src/fields/field-clipboard.ts +132 -0
- package/src/fields/field-layout.svelte +60 -0
- package/src/fields/field-layout.ts +57 -0
- package/src/fields/field-messages.svelte +29 -0
- package/src/fields/field-renderer.svelte +152 -0
- package/src/fields/field-shell.svelte +30 -0
- package/src/fields/field-tab-layout.svelte +99 -0
- package/src/fields/join/join-field.svelte +358 -0
- package/src/fields/localized-field-presentation.ts +17 -0
- package/src/fields/nested/nested-field.svelte +602 -0
- package/src/fields/nested/scoped-field.ts +78 -0
- package/src/fields/nested/sortable-row.svelte +32 -0
- package/src/fields/point/point-field.svelte +52 -0
- package/src/fields/radio/radio-field.svelte +34 -0
- package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
- package/src/fields/relationship/relationship-field.svelte +258 -0
- package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
- package/src/fields/relationship/relationship-query.ts +88 -0
- package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
- package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
- package/src/fields/relationship/relationship-value.ts +47 -0
- package/src/fields/scalar/date-control-value.ts +60 -0
- package/src/fields/scalar/date-value.ts +26 -0
- package/src/fields/scalar/scalar-field.svelte +137 -0
- package/src/fields/select/select-field.svelte +132 -0
- package/src/fields/select/select-value.ts +14 -0
- package/src/fields/text/slug-field.svelte +87 -0
- package/src/fields/text/slug.ts +38 -0
- package/src/fields/text/text-field.svelte +41 -0
- package/src/fields/ui/ui-field.svelte +12 -0
- package/src/fields/virtual/virtual-field.svelte +28 -0
- package/src/index.ts +49 -0
- package/src/main.ts +12 -0
- 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
|
+
/>
|