@riducms/admin 0.2.2 → 0.3.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/THIRD_PARTY_NOTICES.md +60 -37
- package/package.json +27 -15
- package/src/app/admin-core-view-route.svelte +38 -53
- package/src/app/admin-extension-route.svelte +19 -0
- package/src/app/admin-layout.scss +175 -0
- package/src/app/admin-layout.svelte +144 -410
- package/src/app/admin-navigation-progress.svelte +11 -77
- package/src/app/admin-page-title.svelte +17 -85
- package/src/app/admin-page-title.ts +136 -0
- package/src/app/admin-route-error.svelte +24 -0
- package/src/app/admin-router-provider.svelte +34 -0
- package/src/app/admin-router-root.svelte +41 -0
- package/src/app/admin-routes.svelte +56 -19
- package/src/app/loading-boundary.svelte +3 -1
- package/src/app-meta.ts +13 -0
- package/src/app.css +138 -496
- package/src/app.svelte +60 -26
- package/src/components/brand/ridu-logo.svelte +19 -5
- package/src/components/data/list-table.scss +63 -0
- package/src/components/icons/LICENSE.md +24 -0
- package/src/components/icons/chevron.svelte +8 -0
- package/src/components/icons/plus.svelte +12 -0
- package/src/components/icons/search.svelte +12 -0
- package/src/components/icons/x.svelte +8 -0
- package/src/components/json-tree/json-tree-node.svelte +61 -67
- package/src/components/json-tree/json-viewer.scss +153 -0
- package/src/components/json-tree/json-viewer.svelte +57 -96
- package/src/components/ui/banner/banner.scss +28 -0
- package/src/components/ui/banner/banner.svelte +9 -8
- package/src/components/ui/banner/index.ts +1 -5
- package/src/components/ui/code-editor/code-editor.scss +20 -0
- package/src/components/ui/code-editor/code-editor.svelte +71 -0
- package/src/components/ui/code-editor/code-editor.ts +151 -0
- package/src/components/ui/code-editor/code-languages.ts +29 -0
- package/src/components/ui/collapsible-panel/collapsible-panel.scss +38 -0
- package/src/components/ui/collapsible-panel/collapsible-panel.svelte +13 -0
- package/src/components/ui/collapsible-panel/index.ts +1 -0
- package/src/components/ui/combobox/combobox.scss +163 -0
- package/src/components/ui/combobox/index.ts +22 -0
- package/src/components/ui/date-value-control/date-value-calendar.svelte +128 -16
- package/src/components/ui/date-value-control/date-value-control-loader.scss +26 -0
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +35 -13
- package/src/components/ui/date-value-control/date-value-control.scss +223 -0
- package/src/components/ui/date-value-control/date-value-control.svelte +62 -44
- package/src/components/ui/drawer/drawer-depth.ts +8 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -5
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +2 -5
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -2
- package/src/components/ui/dropdown-menu/dropdown-menu.scss +62 -0
- package/src/components/ui/kbd/kbd.scss +17 -0
- package/src/components/ui/kbd/kbd.svelte +3 -10
- package/src/components/ui/navigation-drawer/navigation-drawer.scss +40 -0
- package/src/components/ui/navigation-drawer/navigation-drawer.svelte +38 -0
- package/src/components/ui/popover/popover-content.svelte +3 -5
- package/src/components/ui/popover/popover.scss +18 -0
- package/src/components/ui/radio-group/index.ts +0 -1
- package/src/components/ui/radio-group/radio-card-item.svelte +7 -12
- package/src/components/ui/radio-group/radio-group.svelte +2 -2
- package/src/components/ui/radio-group/radio.scss +45 -0
- package/src/components/ui/select/select-content.svelte +5 -6
- package/src/components/ui/select/select-item.svelte +8 -8
- package/src/components/ui/select/select-scroll-down-button.svelte +3 -5
- package/src/components/ui/select/select-scroll-up-button.svelte +3 -5
- package/src/components/ui/select/select-trigger.svelte +6 -7
- package/src/components/ui/select/select.scss +126 -0
- package/src/components/ui/sheet/sheet-content.svelte +6 -34
- package/src/components/ui/sheet/sheet-description.svelte +2 -2
- package/src/components/ui/sheet/sheet-overlay.svelte +2 -21
- package/src/components/ui/sheet/sheet-title.svelte +2 -2
- package/src/components/ui/sheet/sheet.scss +221 -0
- package/src/components/ui/skeleton/skeleton.scss +20 -0
- package/src/components/ui/skeleton/skeleton.svelte +4 -16
- package/src/components/ui/sonner/sonner.scss +121 -0
- package/src/components/ui/sonner/sonner.svelte +11 -10
- package/src/components/ui/status-indicator/status-indicator.scss +48 -0
- package/src/components/ui/status-indicator/status-indicator.svelte +14 -14
- package/src/components/ui/tabs/tabs-content.svelte +2 -2
- package/src/components/ui/tabs/tabs-list.svelte +7 -7
- package/src/components/ui/tabs/tabs-trigger.svelte +2 -8
- package/src/components/ui/tabs/tabs.scss +86 -0
- package/src/components/ui/tabs/tabs.svelte +2 -2
- package/src/components/ui/timezone-picker/timezone-picker.svelte +152 -0
- package/src/core/api/admin-client.ts +2 -2
- package/src/core/bootstrap/admin-bootstrap.ts +396 -0
- package/src/core/bootstrap/admin-prepared-state.ts +213 -0
- package/src/core/bootstrap/admin-route-modules.ts +66 -0
- package/src/core/clipboard/copy-feedback.svelte.ts +43 -0
- package/src/core/forms/embedded-fields.ts +4 -8
- package/src/core/forms/field-editor-authoring.ts +8 -0
- package/src/core/forms/field-issue-focus.ts +4 -4
- package/src/core/forms/field-occurrence.ts +4 -7
- package/src/core/forms/form-controller.svelte.ts +14 -21
- package/src/core/forms/form-draft-recovery.ts +17 -4
- package/src/core/forms/form-issue-correlation.ts +13 -13
- package/src/core/forms/form-path.ts +17 -0
- package/src/core/forms/form-schema.ts +12 -43
- package/src/core/forms/form-validation.ts +7 -46
- package/src/core/forms/plugin-field-authoring.ts +6 -2
- package/src/core/forms/plugin-field-write.ts +2 -6
- package/src/core/i18n/admin-i18n.svelte.ts +18 -0
- package/src/core/i18n/format-admin-date-time.ts +19 -0
- package/src/core/i18n/humanize-identifier.ts +5 -0
- package/src/core/i18n/time-zone-label.ts +20 -0
- package/src/core/loaders/admin-view-host.svelte +72 -0
- package/src/core/loaders/admin-view-loader.svelte.ts +90 -0
- package/src/core/notifications/validation-toast.svelte +2 -2
- package/src/core/plugins/field-registry.ts +7 -8
- package/src/core/routing/admin-paths.ts +53 -2
- package/src/core/routing/admin-scroll.svelte.ts +74 -0
- package/src/core/routing/admin-view-selection.ts +78 -0
- package/src/core/runtime/admin-runtime.svelte.ts +121 -124
- package/src/core/schema/field-default.ts +33 -0
- package/src/core/schema/field-localization.ts +15 -0
- package/src/core/schema/read-document-path.ts +11 -0
- package/src/features/account/account-menu.scss +37 -0
- package/src/features/account/account-menu.svelte +115 -0
- package/src/features/{auth → account}/account-navigation.svelte +16 -19
- package/src/features/{auth → account}/account-route.svelte +206 -49
- package/src/features/account/account-security-controller.svelte.ts +157 -0
- package/src/features/{auth → account}/account-security-route.svelte +140 -42
- package/src/features/api-reference/api-reference-code.svelte +64 -0
- package/src/features/api-reference/api-reference-content.svelte +258 -0
- package/src/features/api-reference/api-reference-drawer.scss +74 -0
- package/src/features/api-reference/api-reference-examples.ts +381 -0
- package/src/features/api-reference/api-reference-schema.ts +332 -0
- package/src/features/api-reference/api-reference.scss +190 -0
- package/src/features/api-reference/api-reference.svelte +74 -0
- package/src/features/auth/auth-brand.svelte +5 -3
- package/src/features/auth/auth-field.svelte +92 -0
- package/src/features/auth/auth-frame.svelte +5 -8
- package/src/features/auth/auth.scss +122 -0
- package/src/features/auth/create-first-user-controller.svelte.ts +20 -51
- package/src/features/auth/create-first-user-route.svelte +83 -122
- package/src/features/auth/forgot-password-route.svelte +31 -23
- package/src/features/auth/login-route.svelte +68 -116
- package/src/features/auth/logout-button.scss +34 -0
- package/src/features/auth/logout-button.svelte +52 -0
- package/src/features/auth/request-verification-route.svelte +27 -22
- package/src/features/auth/reset-password-route.svelte +108 -41
- package/src/features/auth/verify-email-route.svelte +49 -15
- package/src/features/bulk-edit/bulk-edit-fields.ts +23 -0
- package/src/features/bulk-edit/bulk-edit.scss +152 -0
- package/src/features/bulk-edit/field-picker.svelte +176 -0
- package/src/features/collections/bulk/bulk-actions.svelte +148 -0
- package/src/features/collections/bulk/bulk-confirmation.svelte +153 -0
- package/src/features/collections/bulk/bulk-editor-loader.svelte +95 -0
- package/src/features/collections/bulk/bulk-editor.svelte +186 -0
- package/src/features/collections/collection-list-cell-values.ts +144 -0
- package/src/features/collections/collection-list-controller.svelte.ts +268 -379
- package/src/features/collections/collection-list-preferences-controller.svelte.ts +54 -40
- package/src/features/collections/collection-list-query.ts +223 -0
- package/src/features/collections/collection-list-results.svelte +205 -329
- package/src/features/collections/collection-list-route.svelte +143 -884
- package/src/features/collections/collection-list.scss +261 -0
- package/src/features/collections/collection-list.svelte.ts +362 -0
- package/src/features/collections/controls/column-picker.svelte +110 -0
- package/src/features/collections/controls/column-pill.svelte +62 -0
- package/src/features/collections/controls/filter-builder.svelte +193 -0
- package/src/features/collections/controls/filter-row.svelte +142 -0
- package/src/features/collections/controls/list-controls.scss +196 -0
- package/src/features/collections/controls/list-options.svelte +203 -0
- package/src/features/collections/controls/list-pagination.scss +78 -0
- package/src/features/collections/controls/list-pagination.svelte +115 -0
- package/src/features/collections/controls/list-toolbar.svelte +132 -0
- package/src/features/collections/controls/sort-heading.scss +52 -0
- package/src/features/collections/controls/sort-heading.svelte +78 -0
- package/src/features/collections/list-columns.ts +43 -0
- package/src/features/collections/list-workspace.ts +155 -54
- package/src/features/dashboard/dashboard-route.svelte +82 -86
- package/src/features/dashboard/dashboard.scss +131 -0
- package/src/features/development/development-tools.svelte +20 -36
- package/src/features/documents/document-action-bar.svelte +438 -0
- package/src/features/documents/document-api-view-loader.svelte +17 -6
- package/src/features/documents/document-api-view.scss +120 -0
- package/src/features/documents/document-api-view.svelte +169 -454
- package/src/features/documents/document-controller.svelte.ts +526 -613
- package/src/features/documents/document-credentials.scss +37 -0
- package/src/features/documents/document-credentials.svelte +72 -0
- package/src/features/documents/{document-route-dirty.ts → document-credentials.ts} +25 -0
- package/src/features/documents/document-field-sections.scss +39 -0
- package/src/features/documents/document-field-sections.svelte +4 -13
- package/src/features/documents/document-loading.scss +24 -0
- package/src/features/documents/document-lock-controller.svelte.ts +187 -0
- package/src/features/documents/document-operation-dialogs.svelte +110 -0
- package/src/features/documents/document-route-loader.svelte +17 -5
- package/src/features/documents/document-route.svelte +305 -811
- package/src/features/documents/document-schedule.scss +127 -0
- package/src/features/documents/document-schedule.svelte +323 -0
- package/src/features/documents/document-title.ts +25 -0
- package/src/features/documents/document.scss +274 -0
- package/src/features/documents/live-preview-panel.scss +117 -0
- package/src/features/documents/live-preview-panel.svelte +167 -168
- package/src/features/documents/live-preview.ts +2 -10
- package/src/features/documents/upload-document-preview-loader.svelte +26 -23
- package/src/features/localization/content-locale-switcher.scss +43 -0
- package/src/features/localization/content-locale-switcher.svelte +24 -17
- package/src/features/navigation/admin-breadcrumbs.scss +56 -0
- package/src/features/navigation/admin-breadcrumbs.svelte +102 -37
- package/src/features/navigation/admin-command-menu.svelte +38 -15
- package/src/features/navigation/admin-navigation.scss +133 -0
- package/src/features/navigation/admin-navigation.svelte +169 -0
- package/src/features/navigation/breadcrumb-context.svelte.ts +15 -0
- package/src/features/navigation/navigation-preference.svelte.ts +70 -0
- package/src/features/reference-browser/reference-browser-editor.svelte +180 -0
- package/src/features/reference-browser/reference-browser-list.scss +113 -0
- package/src/features/reference-browser/reference-browser-list.svelte +370 -0
- package/src/features/reference-browser/reference-browser-loader.svelte +26 -8
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +335 -236
- package/src/features/reference-browser/reference-browser.scss +217 -0
- package/src/features/reference-browser/reference-browser.svelte +154 -797
- package/src/features/reference-browser/reference-collection-picker.svelte +116 -0
- package/src/features/reference-browser/reference-list-query.ts +35 -0
- package/src/features/uploads/bulk-upload-controller.svelte.ts +604 -0
- package/src/features/uploads/bulk-upload-edit-all.svelte +92 -0
- package/src/features/uploads/bulk-upload-editor.svelte +191 -0
- package/src/features/uploads/bulk-upload-file-picker.svelte +70 -0
- package/src/features/uploads/bulk-upload-remote.svelte +100 -0
- package/src/features/uploads/bulk-upload-route-loader.svelte +54 -0
- package/src/features/uploads/bulk-upload-route.svelte +172 -592
- package/src/features/uploads/bulk-upload-sidebar.svelte +125 -0
- package/src/features/uploads/bulk-upload.scss +526 -0
- package/src/features/uploads/bulk-upload.ts +105 -14
- package/src/features/uploads/crop-surface.ts +150 -0
- package/src/features/uploads/image-editor.svelte +231 -0
- package/src/features/uploads/rendition-preview.svelte +64 -0
- package/src/features/uploads/upload-control.svelte +340 -0
- package/src/features/uploads/upload-document-contracts.ts +30 -0
- package/src/features/uploads/upload-draft.svelte.ts +294 -0
- package/src/features/uploads/upload-metadata-fields.scss +32 -0
- package/src/features/uploads/upload-metadata-fields.svelte +8 -7
- package/src/features/uploads/upload-thumbnail.svelte +47 -0
- package/src/features/uploads/upload.scss +361 -0
- package/src/features/versions/version-comparison.svelte +361 -0
- package/src/features/versions/version-diff-fields.svelte +136 -0
- package/src/features/versions/version-diff.ts +296 -41
- package/src/features/versions/version-history-controller.svelte.ts +311 -0
- package/src/features/versions/version-history-route-loader.svelte +32 -0
- package/src/features/versions/version-history-route.svelte +157 -522
- package/src/features/versions/version-history.scss +444 -0
- package/src/features/versions/version-history.ts +45 -0
- package/src/features/versions/version-table.svelte +91 -0
- package/src/fields/embedded-schema-draft-editor.scss +48 -0
- package/src/fields/embedded-schema-draft-editor.svelte +24 -16
- package/src/fields/field-collapsible.svelte +14 -8
- package/src/fields/field-layout.scss +171 -0
- package/src/fields/field-layout.svelte +3 -5
- package/src/fields/field-renderer.svelte +17 -29
- package/src/fields/field-shell.svelte +1 -1
- package/src/fields/field-tab-layout.svelte +25 -19
- package/src/fields/join/join-field-controller.svelte.ts +513 -0
- package/src/fields/join/join-field-values.ts +111 -0
- package/src/fields/join/join-field.scss +189 -0
- package/src/fields/join/join-field.svelte +164 -332
- package/src/fields/live-validation-feedback.svelte +4 -3
- package/src/fields/local-field-editor.svelte +2 -2
- package/src/fields/nested/block-name-input.svelte +11 -12
- package/src/fields/nested/block-picker.scss +128 -0
- package/src/fields/nested/block-picker.svelte +81 -0
- package/src/fields/nested/block-placeholder.svg +2 -0
- package/src/fields/nested/nested-field.scss +331 -0
- package/src/fields/nested/nested-field.svelte +265 -245
- package/src/fields/nested/sortable-row.svelte +9 -7
- package/src/fields/plugin-field.svelte +4 -6
- package/src/fields/point/point-field.svelte +51 -38
- package/src/fields/primitive-list/primitive-list-field.svelte +63 -19
- package/src/fields/primitive-list/primitive-list.scss +109 -0
- package/src/fields/radio/radio-field.svelte +2 -1
- package/src/fields/relationship/relationship-field-controller.svelte.ts +81 -41
- package/src/fields/relationship/relationship-field.svelte +144 -175
- package/src/fields/relationship/relationship-lookup-controller.svelte.ts +58 -30
- package/src/fields/relationship/relationship-query.ts +2 -10
- package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +36 -62
- package/src/fields/relationship/relationship-quick-picker.svelte +178 -227
- package/src/fields/relationship/relationship-selection.svelte +120 -0
- package/src/fields/relationship/relationship-value.ts +46 -7
- package/src/fields/relationship/relationship.scss +182 -0
- package/src/fields/scalar/code-field.svelte +101 -0
- package/src/fields/scalar/date-control-value.ts +13 -0
- package/src/fields/scalar/scalar-field.svelte +25 -67
- package/src/fields/select/select-field.svelte +132 -94
- package/src/fields/text/slug-field.scss +28 -0
- package/src/fields/text/slug-field.svelte +19 -15
- package/src/fields/text/text-field.svelte +1 -2
- package/src/fields/ui/ui-field.svelte +7 -4
- package/src/fields/virtual/virtual-field.svelte +2 -5
- package/src/index.ts +1 -2
- package/src/routing.ts +2 -0
- package/src/app/deferred-redirect.svelte +0 -23
- package/src/components/code-block/code-block.svelte +0 -107
- package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +0 -50
- package/src/components/ui/bulk-action-bar/index.ts +0 -1
- package/src/components/ui/button/index.ts +0 -8
- package/src/components/ui/calendar/calendar-caption.svelte +0 -72
- package/src/components/ui/calendar/calendar-cell.svelte +0 -20
- package/src/components/ui/calendar/calendar-day.svelte +0 -27
- package/src/components/ui/calendar/calendar-grid-row.svelte +0 -13
- package/src/components/ui/calendar/calendar-grid.svelte +0 -17
- package/src/components/ui/calendar/calendar-head-cell.svelte +0 -20
- package/src/components/ui/calendar/calendar-header.svelte +0 -20
- package/src/components/ui/calendar/calendar-heading.svelte +0 -17
- package/src/components/ui/calendar/calendar-month-select.svelte +0 -45
- package/src/components/ui/calendar/calendar-month.svelte +0 -16
- package/src/components/ui/calendar/calendar-months.svelte +0 -20
- package/src/components/ui/calendar/calendar-nav.svelte +0 -20
- package/src/components/ui/calendar/calendar-next-button.svelte +0 -30
- package/src/components/ui/calendar/calendar-prev-button.svelte +0 -30
- package/src/components/ui/calendar/calendar-year-select.svelte +0 -44
- package/src/components/ui/calendar/calendar.svelte +0 -118
- package/src/components/ui/calendar/index.ts +0 -1
- package/src/components/ui/checkbox/checkbox.svelte +0 -35
- package/src/components/ui/checkbox/index.ts +0 -1
- package/src/components/ui/dialog/dialog-close.svelte +0 -11
- package/src/components/ui/dialog/dialog-content.svelte +0 -52
- package/src/components/ui/dialog/dialog-description.svelte +0 -20
- package/src/components/ui/dialog/dialog-footer.svelte +0 -20
- package/src/components/ui/dialog/dialog-header.svelte +0 -20
- package/src/components/ui/dialog/dialog-overlay.svelte +0 -20
- package/src/components/ui/dialog/dialog-title.svelte +0 -17
- package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
- package/src/components/ui/dialog/dialog.svelte +0 -7
- package/src/components/ui/dialog/index.ts +0 -8
- package/src/components/ui/empty-state/empty-state.svelte +0 -34
- package/src/components/ui/empty-state/index.ts +0 -1
- package/src/components/ui/input/index.ts +0 -1
- package/src/components/ui/pagination/index.ts +0 -1
- package/src/components/ui/pagination/pagination.svelte +0 -88
- package/src/components/ui/radio-group/radio-group-item.svelte +0 -17
- package/src/components/ui/slider/index.ts +0 -3
- package/src/components/ui/slider/slider-content.svelte +0 -59
- package/src/components/ui/slider/slider.svelte +0 -35
- package/src/components/ui/table/index.ts +0 -6
- package/src/components/ui/table/table-body.svelte +0 -20
- package/src/components/ui/table/table-cell.svelte +0 -23
- package/src/components/ui/table/table-head.svelte +0 -23
- package/src/components/ui/table/table-header.svelte +0 -20
- package/src/components/ui/table/table-row.svelte +0 -23
- package/src/components/ui/table/table.svelte +0 -22
- package/src/components/ui/textarea/index.ts +0 -1
- package/src/components/ui/tooltip/index.ts +0 -4
- package/src/components/ui/tooltip/tooltip-content.svelte +0 -50
- package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
- package/src/components/ui/tooltip/tooltip.svelte +0 -7
- package/src/features/auth/account-menu.svelte +0 -159
- package/src/features/auth/account-security-controller.svelte.ts +0 -119
- package/src/features/collections/collection-list-bulk-actions.svelte +0 -143
- package/src/features/collections/collection-list-bulk-editor.svelte +0 -157
- package/src/features/collections/collection-list-filter-popover.svelte +0 -238
- package/src/features/collections/collection-list-workspace-toolbar.svelte +0 -409
- package/src/features/collections/collection-table-skeleton.svelte +0 -44
- package/src/features/documents/document-heading.ts +0 -20
- package/src/features/documents/upload-document-preview.svelte +0 -525
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
ADMIN_PREPARED_ROUTE_STATE_MEDIA_TYPE,
|
|
4
|
+
ADMIN_PREPARED_ROUTE_STATE_VERSION,
|
|
5
|
+
type AdminPreparedRouteDataV1,
|
|
6
|
+
type AdminCreateDataV1,
|
|
7
|
+
type AdminPreparedRouteStateV1,
|
|
8
|
+
} from "@riducms/protocol";
|
|
9
|
+
import { RiduError, type AdminLoader } from "@riducms/sdk";
|
|
10
|
+
import { createContext } from "svelte";
|
|
11
|
+
|
|
12
|
+
import type { AdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
13
|
+
import { peekFormDraft } from "@admin/core/forms/form-draft-recovery";
|
|
14
|
+
import { recoverFormDraft } from "@admin/core/forms/form-schema";
|
|
15
|
+
import { preparedCustomView } from "@admin/core/routing/admin-view-selection";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
readEmbeddedAdminState,
|
|
19
|
+
validateAdminPreparedState,
|
|
20
|
+
normalizedURLIdentity,
|
|
21
|
+
} from "@admin/core/bootstrap/admin-prepared-state";
|
|
22
|
+
import {
|
|
23
|
+
moduleGroupsForRelativeURL,
|
|
24
|
+
preloadAdminModuleGroups,
|
|
25
|
+
} from "@admin/core/bootstrap/admin-route-modules";
|
|
26
|
+
|
|
27
|
+
export class AdminBootstrapCoordinator {
|
|
28
|
+
#initialState = readEmbeddedAdminState();
|
|
29
|
+
#contextKey = "";
|
|
30
|
+
#routeKey = "";
|
|
31
|
+
#routeData?: {
|
|
32
|
+
identity: string;
|
|
33
|
+
data: AdminPreparedRouteDataV1;
|
|
34
|
+
loaders: AdminPreparedRouteStateV1["loaders"];
|
|
35
|
+
};
|
|
36
|
+
#runtime?: AdminRuntime;
|
|
37
|
+
#basePath = "/admin";
|
|
38
|
+
|
|
39
|
+
get initialState() {
|
|
40
|
+
return this.#initialState;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
configure(runtime: AdminRuntime) {
|
|
44
|
+
this.#runtime = runtime;
|
|
45
|
+
this.#basePath = normalizeAdminBasePath(runtime.adminBasePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get contextKey() {
|
|
49
|
+
return this.#contextKey;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async prepareInitial(options: { stageRoute?: boolean } = {}) {
|
|
53
|
+
const state = this.initialState;
|
|
54
|
+
if (state === undefined) return;
|
|
55
|
+
|
|
56
|
+
if (state.outcome === "redirect") {
|
|
57
|
+
window.location.replace(state.location);
|
|
58
|
+
return neverSettles();
|
|
59
|
+
}
|
|
60
|
+
if (state.outcome === "reload") {
|
|
61
|
+
window.location.reload();
|
|
62
|
+
return neverSettles();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options.stageRoute === false) {
|
|
66
|
+
this.clear();
|
|
67
|
+
this.#initialState = undefined;
|
|
68
|
+
this.#clearContext();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.#adoptContext(state);
|
|
73
|
+
const preparedState = await this.#prepareRestoredCreateDraft(state);
|
|
74
|
+
await preloadAdminModuleGroups(preparedState.moduleGroups);
|
|
75
|
+
if (preparedState.outcome === "prepared") this.stage(preparedState);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
loader = async ({ request }: { request: Request }) => {
|
|
79
|
+
const url = new URL(request.url);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Go marks its HTML even when the initial snapshot is missing or invalid.
|
|
83
|
+
// Browser-only hosts such as Vite have neither that marker nor the endpoint.
|
|
84
|
+
if (
|
|
85
|
+
this.#contextKey === "" &&
|
|
86
|
+
document.querySelector('meta[name="ridu-admin-build-id"]') === null
|
|
87
|
+
) {
|
|
88
|
+
await preloadAdminModuleGroups(
|
|
89
|
+
moduleGroupsForRelativeURL(this.#relativePath(url.pathname))
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Start URL-known chunks while Go reads the data. The response may name
|
|
95
|
+
// additional chunks (such as an upload preview), which we await below.
|
|
96
|
+
const [state] = await Promise.all([
|
|
97
|
+
this.#fetchRouteState(url, request.signal),
|
|
98
|
+
preloadAdminModuleGroups(moduleGroupsForRelativeURL(this.#relativePath(url.pathname))),
|
|
99
|
+
]);
|
|
100
|
+
if (request.signal.aborted) return null;
|
|
101
|
+
|
|
102
|
+
// Recover a missing or invalidated Go context through a fresh document;
|
|
103
|
+
// navigation data cannot establish an authoritative runtime on its own.
|
|
104
|
+
if (state.outcome === "reload" || this.#contextKey === "") {
|
|
105
|
+
window.location.assign(url.href);
|
|
106
|
+
return neverSettles();
|
|
107
|
+
}
|
|
108
|
+
if (state.outcome === "redirect") {
|
|
109
|
+
window.location.assign(state.location);
|
|
110
|
+
return neverSettles();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const preparedState = await this.#prepareRestoredCreateDraft(state, request.signal);
|
|
114
|
+
await preloadAdminModuleGroups(preparedState.moduleGroups);
|
|
115
|
+
if (request.signal.aborted) return null;
|
|
116
|
+
|
|
117
|
+
// Returning data does not publish it. Only commit() may give it to the
|
|
118
|
+
// destination; a newer navigation can still supersede this loader.
|
|
119
|
+
return preparedState;
|
|
120
|
+
} catch {
|
|
121
|
+
if (request.signal.aborted) return null;
|
|
122
|
+
|
|
123
|
+
// Preparation is an optimization. Any fetch, validation, or decode failure leaves the
|
|
124
|
+
// destination's existing controller loader as the authoritative fallback.
|
|
125
|
+
this.clear();
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
shouldRevalidate = ({ currentUrl, nextUrl }: { currentUrl: URL; nextUrl: URL }) =>
|
|
131
|
+
normalizedURLIdentity(currentUrl) !== normalizedURLIdentity(nextUrl);
|
|
132
|
+
|
|
133
|
+
stage(
|
|
134
|
+
state: AdminPreparedRouteStateV1,
|
|
135
|
+
expectedIdentity = normalizedURLIdentity(new URL(window.location.href))
|
|
136
|
+
) {
|
|
137
|
+
const identity = `${state.pathname}${state.search}`;
|
|
138
|
+
if (
|
|
139
|
+
state.version !== ADMIN_PREPARED_ROUTE_STATE_VERSION ||
|
|
140
|
+
expectedIdentity !== identity ||
|
|
141
|
+
state.fingerprint === "" ||
|
|
142
|
+
(this.#contextKey !== "" && state.contextKey !== this.#contextKey) ||
|
|
143
|
+
state.route === undefined ||
|
|
144
|
+
!this.#canSeedRoute(state)
|
|
145
|
+
) {
|
|
146
|
+
this.clear();
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.#routeData = { identity, data: state.route, loaders: state.loaders };
|
|
151
|
+
this.#routeKey = routeStateKey(state);
|
|
152
|
+
this.#adoptContext(state);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
commit(state: AdminPreparedRouteStateV1 | null | undefined, url: URL) {
|
|
157
|
+
if (state == null) {
|
|
158
|
+
this.clear();
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const identity = normalizedURLIdentity(url);
|
|
163
|
+
if (`${state.pathname}${state.search}` !== identity) return false;
|
|
164
|
+
if (this.#contextKey !== "" && state.contextKey !== this.#contextKey) return false;
|
|
165
|
+
|
|
166
|
+
const navigation = state.navigation ?? state.runtime;
|
|
167
|
+
if (navigation !== undefined) this.#runtime?.adoptPreparedNavigation(navigation);
|
|
168
|
+
this.#adoptContext(state);
|
|
169
|
+
if (state.outcome === "prepared") {
|
|
170
|
+
return this.#routeKey === routeStateKey(state) || this.stage(state, identity);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.clear();
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
clear() {
|
|
178
|
+
this.#routeKey = "";
|
|
179
|
+
this.#routeData = undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
routeData(pathname: string, search = "") {
|
|
183
|
+
const base = this.#basePath === "/" ? "" : this.#basePath;
|
|
184
|
+
const url = new URL(`${base}${pathname}${search}`, window.location.origin);
|
|
185
|
+
return this.#routeData?.identity === normalizedURLIdentity(url)
|
|
186
|
+
? this.#routeData.data
|
|
187
|
+
: undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
discardRouteData(state: AdminPreparedRouteStateV1) {
|
|
191
|
+
if (this.#routeKey === routeStateKey(state)) this.clear();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
loaderData(key: string, pathname: string, search = "") {
|
|
195
|
+
return this.routeData(pathname, search) === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: this.#routeData?.loaders?.[key];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
isRouteStaged(state: AdminPreparedRouteStateV1) {
|
|
201
|
+
return this.#routeKey === routeStateKey(state);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async #fetchRouteState(url: URL, signal: AbortSignal) {
|
|
205
|
+
const response = await fetch(url, {
|
|
206
|
+
headers: {
|
|
207
|
+
Accept: ADMIN_PREPARED_ROUTE_STATE_MEDIA_TYPE,
|
|
208
|
+
"Ridu-Admin-Context": this.#contextKey,
|
|
209
|
+
},
|
|
210
|
+
credentials: "include",
|
|
211
|
+
signal,
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) throw new Error(`Admin route preparation failed with ${response.status}.`);
|
|
214
|
+
|
|
215
|
+
return validateAdminPreparedState(await response.json(), url, this.#contextKey);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async #prepareRestoredCreateDraft(state: AdminPreparedRouteStateV1, signal?: AbortSignal) {
|
|
219
|
+
if (state.outcome !== "prepared" || state.route?.kind !== "collection-create") return state;
|
|
220
|
+
|
|
221
|
+
const match = /^\/collections\/([^/]+)\/create(?:\/api)?$/.exec(
|
|
222
|
+
this.#relativePath(state.pathname)
|
|
223
|
+
);
|
|
224
|
+
const manifest = state.runtime?.manifest ?? this.#runtime?.manifest;
|
|
225
|
+
const navigation = state.navigation ?? state.runtime;
|
|
226
|
+
const client = this.#runtime?.client;
|
|
227
|
+
if (match === null || manifest === undefined || client === undefined) return state;
|
|
228
|
+
|
|
229
|
+
const slug = decodeURIComponent(match[1]);
|
|
230
|
+
const collection = manifest.collections.find((candidate) => candidate.slug === slug);
|
|
231
|
+
if (collection === undefined) return state;
|
|
232
|
+
const checkpoint = peekFormDraft(collection.id, undefined);
|
|
233
|
+
if (checkpoint === undefined) return state;
|
|
234
|
+
|
|
235
|
+
const defaults = state.route.create.values;
|
|
236
|
+
const recovered = recoverFormDraft(
|
|
237
|
+
{ values: defaults, original: defaults },
|
|
238
|
+
{ values: checkpoint.values, original: checkpoint.original },
|
|
239
|
+
checkpoint.collection.fields,
|
|
240
|
+
collection.fields
|
|
241
|
+
);
|
|
242
|
+
if (samePreparedCreateValues(recovered.values, defaults)) {
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Go checked access against its defaults. A local draft can change a
|
|
247
|
+
// data-dependent rule, so settle access for the recovered values before reveal.
|
|
248
|
+
let access: AdminCreateDataV1["access"];
|
|
249
|
+
try {
|
|
250
|
+
const value = await client.collectionAccess(slug, {
|
|
251
|
+
data: recovered.values,
|
|
252
|
+
locale: navigation.contentLocale,
|
|
253
|
+
signal,
|
|
254
|
+
});
|
|
255
|
+
access = { value };
|
|
256
|
+
} catch (cause) {
|
|
257
|
+
if (!(cause instanceof RiduError)) throw cause;
|
|
258
|
+
access = {
|
|
259
|
+
error: {
|
|
260
|
+
code: cause.code,
|
|
261
|
+
status: cause.status,
|
|
262
|
+
message: cause.message,
|
|
263
|
+
issues: [...cause.issues],
|
|
264
|
+
...(cause.requestId === undefined ? {} : { requestId: cause.requestId }),
|
|
265
|
+
...(cause.details === undefined ? {} : { details: cause.details }),
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
...state,
|
|
272
|
+
route: {
|
|
273
|
+
...state.route,
|
|
274
|
+
create: { values: recovered.values, access },
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#adoptContext(state: AdminPreparedRouteStateV1) {
|
|
280
|
+
if (state.contextKey === "") return;
|
|
281
|
+
if (this.#contextKey !== "" && this.#contextKey !== state.contextKey) return;
|
|
282
|
+
this.#contextKey = state.contextKey;
|
|
283
|
+
document.documentElement.dataset.riduContext = state.contextKey;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#clearContext() {
|
|
287
|
+
this.#contextKey = "";
|
|
288
|
+
delete document.documentElement.dataset.riduContext;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#canSeedRoute(state: AdminPreparedRouteStateV1) {
|
|
292
|
+
const config = this.#runtime?.config;
|
|
293
|
+
const route = state.route;
|
|
294
|
+
if (config === undefined || route === undefined) return route !== undefined;
|
|
295
|
+
|
|
296
|
+
const selected = preparedCustomView(
|
|
297
|
+
config,
|
|
298
|
+
this.#relativePath(state.pathname),
|
|
299
|
+
this.#runtime?.manifest
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (route.kind === "custom") {
|
|
303
|
+
const loader = selected?.loader;
|
|
304
|
+
return loader !== undefined && validLoaderSeed(loader, state.loaders);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// A core seed belongs to the core view, never to its custom replacement.
|
|
308
|
+
if (selected !== undefined) return false;
|
|
309
|
+
|
|
310
|
+
switch (route.kind) {
|
|
311
|
+
case "dashboard": {
|
|
312
|
+
const panels = config.extensions.dashboardPanels;
|
|
313
|
+
const replacement = panels.find((panel) => panel.position === "replace");
|
|
314
|
+
if (replacement !== undefined) {
|
|
315
|
+
return (
|
|
316
|
+
replacement.loader !== undefined && validLoaderSeed(replacement.loader, state.loaders)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return panels.every(
|
|
321
|
+
(panel) => panel.loader === undefined || validLoaderSeed(panel.loader, state.loaders)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
case "account":
|
|
325
|
+
case "security":
|
|
326
|
+
return !config.extensions.account.some((panel) => panel.position === "replace");
|
|
327
|
+
case "login":
|
|
328
|
+
return !config.extensions.login.some((panel) => panel.position === "replace");
|
|
329
|
+
default:
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#relativePath(pathname: string) {
|
|
335
|
+
if (this.#basePath === "/") return pathname;
|
|
336
|
+
if (pathname === this.#basePath) return "/";
|
|
337
|
+
return pathname.startsWith(`${this.#basePath}/`)
|
|
338
|
+
? pathname.slice(this.#basePath.length)
|
|
339
|
+
: pathname;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validLoaderSeed(
|
|
344
|
+
loader: AdminLoader<never, unknown>,
|
|
345
|
+
results: AdminPreparedRouteStateV1["loaders"]
|
|
346
|
+
) {
|
|
347
|
+
const result = results?.[loader.key];
|
|
348
|
+
if (result === undefined) return false;
|
|
349
|
+
// A structured read failure is complete data too: the view renders its error state.
|
|
350
|
+
if (result.error !== undefined) return true;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
loader.decode(result.value);
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function routeStateKey(state: AdminPreparedRouteStateV1) {
|
|
361
|
+
return `${state.pathname}${state.search}:${state.fingerprint}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Create access depends on values, including a recovered local draft. Object
|
|
365
|
+
// key order is not data identity, and authored fields named "signal" are data.
|
|
366
|
+
export function samePreparedCreateValues(left: unknown, right: unknown): boolean {
|
|
367
|
+
if (left === right) return true;
|
|
368
|
+
if (Array.isArray(left) && Array.isArray(right))
|
|
369
|
+
return (
|
|
370
|
+
left.length === right.length &&
|
|
371
|
+
left.every((value, index) => samePreparedCreateValues(value, right[index]))
|
|
372
|
+
);
|
|
373
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
374
|
+
const keys = Object.keys(left);
|
|
375
|
+
return (
|
|
376
|
+
keys.length === Object.keys(right).length &&
|
|
377
|
+
keys.every(
|
|
378
|
+
(key) => Object.hasOwn(right, key) && samePreparedCreateValues(left[key], right[key])
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeAdminBasePath(basePath: string) {
|
|
384
|
+
return `/${basePath.split("/").filter(Boolean).join("/")}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function neverSettles(): Promise<never> {
|
|
388
|
+
// A hard navigation transfers ownership to a new document, so the initiating handler must not
|
|
389
|
+
// resume and mutate the outgoing application while that document is loading.
|
|
390
|
+
return new Promise(() => {});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const [getAdminBootstrapCoordinator, setAdminBootstrapCoordinator] =
|
|
394
|
+
createContext<AdminBootstrapCoordinator>();
|
|
395
|
+
|
|
396
|
+
export { getAdminBootstrapCoordinator, setAdminBootstrapCoordinator };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
ADMIN_PREPARED_ROUTE_STATE_VERSION,
|
|
4
|
+
isAdminPreparedRouteData,
|
|
5
|
+
isAdminLoaderResults,
|
|
6
|
+
type AdminPreparedRouteStateV1,
|
|
7
|
+
type AdminPreparedRouteDataV1,
|
|
8
|
+
} from "@riducms/protocol";
|
|
9
|
+
|
|
10
|
+
const templateID = "ridu-admin-initial-state";
|
|
11
|
+
|
|
12
|
+
// Cold HTML and navigation JSON cross the same wire boundary. Validate them
|
|
13
|
+
// here, before the coordinator exposes anything to the runtime or controllers.
|
|
14
|
+
export function readEmbeddedAdminState(): AdminPreparedRouteStateV1 | undefined {
|
|
15
|
+
const template = document.getElementById(templateID);
|
|
16
|
+
if (!(template instanceof HTMLTemplateElement)) return undefined;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return validateAdminPreparedState(
|
|
20
|
+
JSON.parse(template.content.textContent ?? ""),
|
|
21
|
+
new URL(location.href)
|
|
22
|
+
);
|
|
23
|
+
} catch {
|
|
24
|
+
// Embedded state is optional; malformed or stale data falls back to the SDK bootstrap.
|
|
25
|
+
return undefined;
|
|
26
|
+
} finally {
|
|
27
|
+
template.remove();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validateAdminPreparedState(
|
|
32
|
+
value: unknown,
|
|
33
|
+
url: URL,
|
|
34
|
+
contextKey?: string
|
|
35
|
+
): AdminPreparedRouteStateV1 {
|
|
36
|
+
if (!isRecord(value)) throw new Error("Admin prepared state must be an object.");
|
|
37
|
+
if (value.version !== ADMIN_PREPARED_ROUTE_STATE_VERSION)
|
|
38
|
+
throw new Error("Admin prepared state version is incompatible.");
|
|
39
|
+
|
|
40
|
+
if (typeof value.pathname !== "string" || typeof value.search !== "string")
|
|
41
|
+
throw new Error("Admin prepared route identity is invalid.");
|
|
42
|
+
if (`${value.pathname}${value.search}` !== normalizedURLIdentity(url))
|
|
43
|
+
throw new Error("Admin prepared route identity is stale.");
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
typeof value.contextKey !== "string" ||
|
|
47
|
+
(value.contextKey !== "" && !/^[0-9a-f]{32}$/.test(value.contextKey))
|
|
48
|
+
)
|
|
49
|
+
throw new Error("Admin prepared context key is invalid.");
|
|
50
|
+
if (
|
|
51
|
+
typeof value.fingerprint !== "string" ||
|
|
52
|
+
(value.fingerprint !== "" && !/^[0-9a-f]{64}$/.test(value.fingerprint))
|
|
53
|
+
)
|
|
54
|
+
throw new Error("Admin prepared fingerprint is invalid.");
|
|
55
|
+
if (
|
|
56
|
+
typeof value.buildId !== "string" ||
|
|
57
|
+
(value.buildId !== "" && !/^[0-9a-f]{24}$/.test(value.buildId))
|
|
58
|
+
)
|
|
59
|
+
throw new Error("Admin prepared build ID is invalid.");
|
|
60
|
+
if (
|
|
61
|
+
!Array.isArray(value.moduleGroups) ||
|
|
62
|
+
!value.moduleGroups.every((group) => typeof group === "string")
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Admin prepared module groups are invalid.");
|
|
65
|
+
|
|
66
|
+
const expectedBuildID = document
|
|
67
|
+
.querySelector<HTMLMetaElement>('meta[name="ridu-admin-build-id"]')
|
|
68
|
+
?.getAttribute("content");
|
|
69
|
+
if (
|
|
70
|
+
value.outcome !== "reload" &&
|
|
71
|
+
// An empty navigation context requires a fresh document, including its build.
|
|
72
|
+
contextKey !== "" &&
|
|
73
|
+
expectedBuildID != null &&
|
|
74
|
+
value.buildId !== expectedBuildID
|
|
75
|
+
)
|
|
76
|
+
throw new Error("Admin prepared state build is stale.");
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
value.loaders !== undefined &&
|
|
80
|
+
(value.outcome !== "prepared" || !isAdminLoaderResults(value.loaders))
|
|
81
|
+
)
|
|
82
|
+
throw new Error("Admin loader results are invalid.");
|
|
83
|
+
|
|
84
|
+
// Each outcome has a different payload. Keep those requirements separate so
|
|
85
|
+
// adding a field to one outcome cannot silently relax another's validation.
|
|
86
|
+
switch (value.outcome) {
|
|
87
|
+
case "prepared":
|
|
88
|
+
if (
|
|
89
|
+
!isAdminPreparedRouteData(value.route) ||
|
|
90
|
+
(value.runtime === undefined && value.navigation === undefined) ||
|
|
91
|
+
!validPreparedModuleGroups(value.moduleGroups, value.route)
|
|
92
|
+
)
|
|
93
|
+
throw new Error("Admin prepared route data is invalid.");
|
|
94
|
+
break;
|
|
95
|
+
case "redirect":
|
|
96
|
+
if (typeof value.location !== "string")
|
|
97
|
+
throw new Error("Admin prepared redirect location is invalid.");
|
|
98
|
+
break;
|
|
99
|
+
case "reload":
|
|
100
|
+
if (
|
|
101
|
+
value.runtime !== undefined ||
|
|
102
|
+
value.navigation !== undefined ||
|
|
103
|
+
value.route !== undefined
|
|
104
|
+
)
|
|
105
|
+
throw new Error("Admin reload must not carry runtime or route data.");
|
|
106
|
+
break;
|
|
107
|
+
case "fallback":
|
|
108
|
+
if (value.location !== undefined || !validDiagnostic(value.diagnostic))
|
|
109
|
+
throw new Error("Admin fallback requires a diagnostic and no redirect.");
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
throw new Error("Admin prepared state outcome is invalid.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (value.runtime !== undefined && !validPreparedRuntime(value.runtime))
|
|
116
|
+
throw new Error("Admin prepared runtime is invalid.");
|
|
117
|
+
|
|
118
|
+
// Compact navigation can reuse a runtime, but can never establish one.
|
|
119
|
+
if (
|
|
120
|
+
value.navigation !== undefined &&
|
|
121
|
+
(value.runtime !== undefined ||
|
|
122
|
+
!contextKey ||
|
|
123
|
+
value.contextKey !== contextKey ||
|
|
124
|
+
!validPreparedNavigation(value.navigation))
|
|
125
|
+
)
|
|
126
|
+
throw new Error("Admin prepared navigation requires the matching runtime context.");
|
|
127
|
+
|
|
128
|
+
return value as unknown as AdminPreparedRouteStateV1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validPreparedModuleGroups(groups: string[], route: AdminPreparedRouteDataV1) {
|
|
132
|
+
const allowed = new Set([
|
|
133
|
+
"entry",
|
|
134
|
+
"document",
|
|
135
|
+
"date",
|
|
136
|
+
"upload-preview",
|
|
137
|
+
"bulk-upload",
|
|
138
|
+
"document-api",
|
|
139
|
+
"versions",
|
|
140
|
+
]);
|
|
141
|
+
if (
|
|
142
|
+
groups.some((group) => !allowed.has(group)) ||
|
|
143
|
+
new Set(groups).size !== groups.length ||
|
|
144
|
+
!groups.includes("entry")
|
|
145
|
+
)
|
|
146
|
+
return false;
|
|
147
|
+
|
|
148
|
+
switch (route.kind) {
|
|
149
|
+
case "collection-create":
|
|
150
|
+
case "collection-document":
|
|
151
|
+
case "global-document":
|
|
152
|
+
return groups.includes("document");
|
|
153
|
+
case "collection-api":
|
|
154
|
+
case "global-api":
|
|
155
|
+
return groups.includes("document") && groups.includes("document-api");
|
|
156
|
+
case "collection-versions":
|
|
157
|
+
case "global-versions":
|
|
158
|
+
return groups.includes("versions");
|
|
159
|
+
case "upload":
|
|
160
|
+
return groups.includes("bulk-upload");
|
|
161
|
+
case "security":
|
|
162
|
+
return groups.includes("date");
|
|
163
|
+
default:
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validPreparedNavigation(value: unknown) {
|
|
169
|
+
return (
|
|
170
|
+
isRecord(value) &&
|
|
171
|
+
isRecord(value.collectionOperations) &&
|
|
172
|
+
isRecord(value.globalOperations) &&
|
|
173
|
+
(value.contentLocale === undefined || typeof value.contentLocale === "string")
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validPreparedRuntime(value: unknown) {
|
|
178
|
+
if (!isRecord(value) || !isRecord(value.manifest) || !isRecord(value.manifest.application))
|
|
179
|
+
return false;
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
!Array.isArray(value.manifest.collections) ||
|
|
183
|
+
(value.manifest.globals !== undefined && !Array.isArray(value.manifest.globals)) ||
|
|
184
|
+
!Array.isArray(value.manifest.plugins) ||
|
|
185
|
+
typeof value.authBootstrapAvailable !== "boolean" ||
|
|
186
|
+
!validPreparedNavigation(value) ||
|
|
187
|
+
!isRecord(value.preferences) ||
|
|
188
|
+
!["system", "light", "dark"].includes(String(value.theme))
|
|
189
|
+
)
|
|
190
|
+
return false;
|
|
191
|
+
|
|
192
|
+
if (value.session === undefined) return true;
|
|
193
|
+
return (
|
|
194
|
+
isRecord(value.session) &&
|
|
195
|
+
typeof value.session.id === "string" &&
|
|
196
|
+
typeof value.session.collection === "string" &&
|
|
197
|
+
isRecord(value.session.user)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validDiagnostic(value: unknown) {
|
|
202
|
+
return isRecord(value) && typeof value.code === "string" && typeof value.message === "string";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function normalizedURLIdentity(url: Pick<URL, "pathname" | "search">) {
|
|
206
|
+
const parameters = new URLSearchParams(url.search);
|
|
207
|
+
// Stable key sorting matches Go's identity and preserves repeated-value order.
|
|
208
|
+
parameters.sort();
|
|
209
|
+
|
|
210
|
+
const search = parameters.size === 0 ? "" : `?${parameters}`;
|
|
211
|
+
const pathname = url.pathname.length > 1 ? url.pathname.replace(/\/$/, "") : url.pathname;
|
|
212
|
+
return pathname + search;
|
|
213
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Keep the exports, not a second import-promise cache: the browser already
|
|
2
|
+
// deduplicates imports. Route wrappers need these synchronously on first render
|
|
3
|
+
// so an already-loaded component does not briefly enter an {#await} placeholder.
|
|
4
|
+
const preparedModules = new Map<string, unknown>();
|
|
5
|
+
|
|
6
|
+
export function preparedAdminModule<Module>(group: string): Module | undefined {
|
|
7
|
+
return preparedModules.get(group) as Module | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function moduleGroupsForRelativeURL(pathname: string) {
|
|
11
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
12
|
+
if (segments[0] === "collections") {
|
|
13
|
+
if (segments.length === 3 && segments[2] === "upload") return ["bulk-upload"];
|
|
14
|
+
if ((segments.length === 4 || segments.length === 5) && segments[3] === "versions") {
|
|
15
|
+
return ["versions"];
|
|
16
|
+
}
|
|
17
|
+
if (segments.length === 3 && segments[2] !== "trash" && segments[2] !== "upload") {
|
|
18
|
+
return ["document"];
|
|
19
|
+
}
|
|
20
|
+
if (segments.length === 4 && segments[3] === "api") return ["document", "document-api"];
|
|
21
|
+
}
|
|
22
|
+
if (segments[0] === "globals") {
|
|
23
|
+
if ((segments.length === 3 || segments.length === 4) && segments[2] === "versions") {
|
|
24
|
+
return ["versions"];
|
|
25
|
+
}
|
|
26
|
+
if (segments.length === 2) return ["document"];
|
|
27
|
+
if (segments.length === 3 && segments[2] === "api") return ["document", "document-api"];
|
|
28
|
+
}
|
|
29
|
+
if (segments.length === 2 && segments[0] === "account" && segments[1] === "security") {
|
|
30
|
+
return ["date"];
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function preloadAdminModuleGroups(groups: readonly string[]) {
|
|
36
|
+
await Promise.all(
|
|
37
|
+
[...new Set(groups)].map(async (group) => {
|
|
38
|
+
if (preparedModules.has(group)) return;
|
|
39
|
+
let module: unknown;
|
|
40
|
+
switch (group) {
|
|
41
|
+
case "bulk-upload":
|
|
42
|
+
module = await import("@admin/features/uploads/bulk-upload-route.svelte");
|
|
43
|
+
break;
|
|
44
|
+
case "document":
|
|
45
|
+
module = await import("@admin/features/documents/document-route.svelte");
|
|
46
|
+
break;
|
|
47
|
+
case "versions":
|
|
48
|
+
module = await import("@admin/features/versions/version-history-route.svelte");
|
|
49
|
+
break;
|
|
50
|
+
case "date":
|
|
51
|
+
module =
|
|
52
|
+
await import("@admin/components/ui/date-value-control/date-value-control.svelte");
|
|
53
|
+
break;
|
|
54
|
+
case "upload-preview":
|
|
55
|
+
module = await import("@admin/features/uploads/upload-control.svelte");
|
|
56
|
+
break;
|
|
57
|
+
case "document-api":
|
|
58
|
+
module = await import("@admin/features/documents/document-api-view.svelte");
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
preparedModules.set(group, module);
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|