@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,625 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveAdminPluginExtensions,
|
|
3
|
+
type AdminDashboardPanel,
|
|
4
|
+
type AdminLoginComponent,
|
|
5
|
+
type AdminAccountComponent,
|
|
6
|
+
type AdminNavigationComponent,
|
|
7
|
+
type AdminLogoutButton,
|
|
8
|
+
type AdminCoreView,
|
|
9
|
+
type AdminBrandComponent,
|
|
10
|
+
type AdminShellComponent,
|
|
11
|
+
type AdminProvider,
|
|
12
|
+
type AdminDocumentAction,
|
|
13
|
+
type AdminDocumentView,
|
|
14
|
+
type AdminListCell,
|
|
15
|
+
type AdminPlugin,
|
|
16
|
+
type AdminPluginRoute,
|
|
17
|
+
type FieldPlugin,
|
|
18
|
+
} from "@riducms/plugin";
|
|
19
|
+
import type { AuthSession, OperationCapabilities, SchemaManifest } from "@riducms/protocol";
|
|
20
|
+
import { RiduError } from "@riducms/sdk";
|
|
21
|
+
import type { TranslationLanguage } from "@riducms/translations";
|
|
22
|
+
import { createContext } from "svelte";
|
|
23
|
+
|
|
24
|
+
import type { AdminClient, AdminDocument } from "@admin/core/api/admin-client";
|
|
25
|
+
import { createCoreFieldRegistry, type FieldRegistry } from "@admin/core/plugins/field-registry";
|
|
26
|
+
import {
|
|
27
|
+
createRowLabelRegistry,
|
|
28
|
+
type RowLabelRegistry,
|
|
29
|
+
} from "@admin/core/plugins/row-label-registry";
|
|
30
|
+
import {
|
|
31
|
+
getPreferenceWriteQueue,
|
|
32
|
+
preferenceOwnerID,
|
|
33
|
+
} from "@admin/core/preferences/preference-write-queue";
|
|
34
|
+
import { AdminI18nController } from "@admin/core/i18n/admin-i18n.svelte";
|
|
35
|
+
|
|
36
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
37
|
+
|
|
38
|
+
export class ContentLocaleSwitchBlockedError extends Error {
|
|
39
|
+
constructor() {
|
|
40
|
+
super("The content locale cannot be changed in the current admin state.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class StalePreferenceSessionError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("The signed-in session changed before the preference could be saved.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class AdminRuntime {
|
|
51
|
+
manifest = $state<SchemaManifest>();
|
|
52
|
+
manifestRevision = $state(0);
|
|
53
|
+
documentRevision = $state(0);
|
|
54
|
+
session = $state<AuthSession<AdminDocument>>();
|
|
55
|
+
authBootstrapAvailable = $state(false);
|
|
56
|
+
loading = $state(true);
|
|
57
|
+
error = $state<string>();
|
|
58
|
+
refreshingManifest = $state(false);
|
|
59
|
+
manifestRefreshError = $state<string>();
|
|
60
|
+
collectionOperations = $state.raw<Record<string, OperationCapabilities>>({});
|
|
61
|
+
globalOperations = $state.raw<Record<string, OperationCapabilities>>({});
|
|
62
|
+
themePreference = $state<ThemePreference>("system");
|
|
63
|
+
resolvedTheme = $state<"light" | "dark">("dark");
|
|
64
|
+
contentLocale = $state<string>();
|
|
65
|
+
#confirmedThemePreference: ThemePreference = "system";
|
|
66
|
+
#confirmedThemeOwnerID = "";
|
|
67
|
+
#confirmedContentLocale?: string;
|
|
68
|
+
#confirmedContentLocaleOwnerID = "";
|
|
69
|
+
#manifestRequest = 0;
|
|
70
|
+
#accessRequest = 0;
|
|
71
|
+
#themeRequest = 0;
|
|
72
|
+
#contentLocaleRequest = 0;
|
|
73
|
+
#contentLocaleBlockers = new Map<symbol, () => boolean>();
|
|
74
|
+
#contentLocaleBlockerSequence = 0;
|
|
75
|
+
#contentLocaleBlockerRevision = $state(0);
|
|
76
|
+
#preferenceReset?: {
|
|
77
|
+
sessionID: string | undefined;
|
|
78
|
+
ownerID: string;
|
|
79
|
+
promise: Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
get authCollection() {
|
|
83
|
+
const admin = this.manifest?.application.admin;
|
|
84
|
+
if (admin === undefined) return undefined;
|
|
85
|
+
return this.manifest?.collections.find(
|
|
86
|
+
(collection) =>
|
|
87
|
+
collection.id === admin.userCollectionId &&
|
|
88
|
+
collection.slug === admin.userCollectionSlug &&
|
|
89
|
+
collection.capabilities.auth
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get authenticationRequired() {
|
|
94
|
+
return this.manifest?.application.admin !== undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get authenticated() {
|
|
98
|
+
return !this.authenticationRequired || (this.session !== undefined && this.adminAccessAllowed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get adminAccessAllowed() {
|
|
102
|
+
if (!this.authenticationRequired) return true;
|
|
103
|
+
const slug = this.authCollection?.slug;
|
|
104
|
+
return slug !== undefined && this.collectionOperations[slug]?.admin === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get visibleCollections() {
|
|
108
|
+
return (this.manifest?.collections ?? []).filter(
|
|
109
|
+
(collection) => this.collectionOperations[collection.slug]?.read === true
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get visibleGlobals() {
|
|
114
|
+
return (this.manifest?.globals ?? []).filter(
|
|
115
|
+
(global) => this.globalOperations[global.slug]?.read === true
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get contentLocales() {
|
|
120
|
+
return this.manifest?.application.localization?.locales ?? [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get contentLocaleSwitchBlocked() {
|
|
124
|
+
this.#contentLocaleBlockerRevision;
|
|
125
|
+
return [...this.#contentLocaleBlockers.values()].some((blocked) => blocked());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
documentsChanged() {
|
|
129
|
+
this.documentRevision += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
registerContentLocaleBlocker(blocked: () => boolean) {
|
|
133
|
+
const owner = Symbol("content-locale-blocker");
|
|
134
|
+
this.#contentLocaleBlockers.set(owner, blocked);
|
|
135
|
+
this.#contentLocaleBlockerRevision = ++this.#contentLocaleBlockerSequence;
|
|
136
|
+
return () => {
|
|
137
|
+
if (!this.#contentLocaleBlockers.delete(owner)) return;
|
|
138
|
+
this.#contentLocaleBlockerRevision = ++this.#contentLocaleBlockerSequence;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
adoptContentLocale(locale: string) {
|
|
143
|
+
if (!this.contentLocales.some((candidate) => candidate.code === locale)) return false;
|
|
144
|
+
if (this.contentLocaleSwitchBlocked) return false;
|
|
145
|
+
if (this.contentLocale === locale) return true;
|
|
146
|
+
this.contentLocale = locale;
|
|
147
|
+
void this.refreshAccess(locale).catch(() => {
|
|
148
|
+
// Route adoption is best-effort; retain the last complete access snapshot on failure.
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
readonly pluginRoutes: readonly AdminPluginRoute[];
|
|
154
|
+
readonly dashboardPanels: readonly AdminDashboardPanel[];
|
|
155
|
+
readonly loginComponents: readonly AdminLoginComponent[];
|
|
156
|
+
readonly accountComponents: readonly AdminAccountComponent[];
|
|
157
|
+
readonly navigationComponents: readonly AdminNavigationComponent[];
|
|
158
|
+
readonly logoutButton?: AdminLogoutButton;
|
|
159
|
+
readonly coreViews: readonly AdminCoreView[];
|
|
160
|
+
readonly brandComponents: readonly AdminBrandComponent[];
|
|
161
|
+
readonly shellComponents: readonly AdminShellComponent[];
|
|
162
|
+
readonly providers: readonly AdminProvider[];
|
|
163
|
+
readonly listCells: readonly AdminListCell[];
|
|
164
|
+
readonly documentActions: readonly AdminDocumentAction[];
|
|
165
|
+
readonly documentViews: readonly AdminDocumentView[];
|
|
166
|
+
readonly rowLabels: RowLabelRegistry;
|
|
167
|
+
readonly i18n: AdminI18nController;
|
|
168
|
+
|
|
169
|
+
constructor(
|
|
170
|
+
readonly client: AdminClient,
|
|
171
|
+
readonly plugins: readonly AdminPlugin[] = [],
|
|
172
|
+
fieldPlugins: readonly FieldPlugin[] = [],
|
|
173
|
+
languages?: readonly TranslationLanguage[],
|
|
174
|
+
readonly fields: FieldRegistry = createCoreFieldRegistry([
|
|
175
|
+
...plugins.flatMap((plugin) => plugin.fields),
|
|
176
|
+
...fieldPlugins,
|
|
177
|
+
])
|
|
178
|
+
) {
|
|
179
|
+
const extensions = resolveAdminPluginExtensions(plugins);
|
|
180
|
+
this.pluginRoutes = extensions.routes;
|
|
181
|
+
this.dashboardPanels = extensions.dashboard;
|
|
182
|
+
this.loginComponents = extensions.login;
|
|
183
|
+
this.accountComponents = extensions.account;
|
|
184
|
+
this.navigationComponents = extensions.navigation;
|
|
185
|
+
this.logoutButton = extensions.logoutButton;
|
|
186
|
+
this.coreViews = extensions.views;
|
|
187
|
+
this.brandComponents = extensions.branding;
|
|
188
|
+
this.shellComponents = extensions.shell;
|
|
189
|
+
this.providers = extensions.providers;
|
|
190
|
+
this.listCells = extensions.listCells;
|
|
191
|
+
this.documentActions = extensions.documentActions;
|
|
192
|
+
this.documentViews = extensions.documentViews;
|
|
193
|
+
this.rowLabels = createRowLabelRegistry(extensions.rowLabels);
|
|
194
|
+
this.i18n = new AdminI18nController(client, () => this.session, languages, extensions.messages);
|
|
195
|
+
this.#applyTheme();
|
|
196
|
+
if (typeof window !== "undefined") {
|
|
197
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
198
|
+
if (this.themePreference === "system") this.#applyTheme();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async loadTheme() {
|
|
204
|
+
const request = ++this.#themeRequest;
|
|
205
|
+
const sessionID = this.session?.id;
|
|
206
|
+
const ownerID = preferenceOwnerID(this.session);
|
|
207
|
+
let preference: unknown;
|
|
208
|
+
let authoritative = this.session === undefined;
|
|
209
|
+
if (this.session !== undefined) {
|
|
210
|
+
try {
|
|
211
|
+
preference = await this.client.preference<unknown>("theme");
|
|
212
|
+
authoritative = true;
|
|
213
|
+
} catch (cause) {
|
|
214
|
+
if (cause instanceof RiduError && cause.code === "not_found") authoritative = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (request !== this.#themeRequest || this.session?.id !== sessionID) return;
|
|
218
|
+
this.#confirmedThemePreference = authoritative
|
|
219
|
+
? isThemePreference(preference)
|
|
220
|
+
? preference
|
|
221
|
+
: "system"
|
|
222
|
+
: this.#confirmedThemeOwnerID === ownerID
|
|
223
|
+
? this.#confirmedThemePreference
|
|
224
|
+
: "system";
|
|
225
|
+
this.#confirmedThemeOwnerID = ownerID;
|
|
226
|
+
this.themePreference = this.#confirmedThemePreference;
|
|
227
|
+
this.#applyTheme();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
contentLocaleForPath(path: string) {
|
|
231
|
+
return contentLocaleFromPath(path, this.contentLocales);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async loadContentLocale(activeLocale?: string) {
|
|
235
|
+
const request = ++this.#contentLocaleRequest;
|
|
236
|
+
const localization = this.manifest?.application.localization;
|
|
237
|
+
if (localization === undefined) {
|
|
238
|
+
this.contentLocale = undefined;
|
|
239
|
+
this.#confirmedContentLocale = undefined;
|
|
240
|
+
this.#confirmedContentLocaleOwnerID = "";
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const sessionID = this.session?.id;
|
|
244
|
+
const ownerID = preferenceOwnerID(this.session);
|
|
245
|
+
let preference: unknown;
|
|
246
|
+
let authoritative = this.session === undefined;
|
|
247
|
+
if (this.session !== undefined) {
|
|
248
|
+
try {
|
|
249
|
+
preference = await this.client.preference<unknown>("content-locale");
|
|
250
|
+
authoritative = true;
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
if (cause instanceof RiduError && cause.code === "not_found") authoritative = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (request !== this.#contentLocaleRequest || this.session?.id !== sessionID) return;
|
|
256
|
+
const preferred = contentLocalePreference(preference, localization.locales);
|
|
257
|
+
this.#confirmedContentLocale = authoritative
|
|
258
|
+
? (preferred ?? localization.defaultLocale)
|
|
259
|
+
: this.#confirmedContentLocaleOwnerID === ownerID
|
|
260
|
+
? (this.#confirmedContentLocale ?? localization.defaultLocale)
|
|
261
|
+
: localization.defaultLocale;
|
|
262
|
+
this.#confirmedContentLocaleOwnerID = ownerID;
|
|
263
|
+
this.contentLocale = this.contentLocales.some((candidate) => candidate.code === activeLocale)
|
|
264
|
+
? activeLocale
|
|
265
|
+
: this.#confirmedContentLocale;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async setContentLocale(locale: string) {
|
|
269
|
+
if (!this.contentLocales.some((candidate) => candidate.code === locale)) return;
|
|
270
|
+
if (this.contentLocaleSwitchBlocked) throw new ContentLocaleSwitchBlockedError();
|
|
271
|
+
if (locale === this.contentLocale && locale === this.#confirmedContentLocale) return;
|
|
272
|
+
const request = ++this.#contentLocaleRequest;
|
|
273
|
+
const sessionID = this.session?.id;
|
|
274
|
+
const ownerID = preferenceOwnerID(this.session);
|
|
275
|
+
this.contentLocale = locale;
|
|
276
|
+
const accessRefresh = this.refreshAccess(locale);
|
|
277
|
+
void accessRefresh.catch(() => {
|
|
278
|
+
// The operation awaits and reconciles this failure after its preference write settles.
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
if (sessionID === undefined || ownerID === "") {
|
|
282
|
+
await this.client.setPreference("content-locale", { locale });
|
|
283
|
+
} else {
|
|
284
|
+
const result = await getPreferenceWriteQueue().enqueue(
|
|
285
|
+
JSON.stringify([ownerID, "content-locale"]),
|
|
286
|
+
ownerID,
|
|
287
|
+
() => this.session?.id === sessionID,
|
|
288
|
+
() => this.client.setPreference("content-locale", { locale })
|
|
289
|
+
);
|
|
290
|
+
if (!result.dispatched) throw new StalePreferenceSessionError();
|
|
291
|
+
}
|
|
292
|
+
if (request !== this.#contentLocaleRequest) return;
|
|
293
|
+
if (this.session?.id !== sessionID || preferenceOwnerID(this.session) !== ownerID) {
|
|
294
|
+
throw new StalePreferenceSessionError();
|
|
295
|
+
}
|
|
296
|
+
await accessRefresh;
|
|
297
|
+
this.#confirmedContentLocale = locale;
|
|
298
|
+
this.#confirmedContentLocaleOwnerID = ownerID;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (request === this.#contentLocaleRequest) {
|
|
301
|
+
this.contentLocale = this.#confirmedContentLocale;
|
|
302
|
+
await this.refreshAccess(this.#confirmedContentLocale);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async setTheme(preference: ThemePreference) {
|
|
309
|
+
const themeRequest = ++this.#themeRequest;
|
|
310
|
+
const sessionID = this.session?.id;
|
|
311
|
+
const ownerID = preferenceOwnerID(this.session);
|
|
312
|
+
const activeReset = this.#preferenceReset;
|
|
313
|
+
if (activeReset !== undefined) {
|
|
314
|
+
if (activeReset.sessionID === sessionID && activeReset.ownerID === ownerID) {
|
|
315
|
+
try {
|
|
316
|
+
await activeReset.promise;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
await this.#reconcileTheme(themeRequest);
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
try {
|
|
323
|
+
await activeReset.promise;
|
|
324
|
+
} catch {
|
|
325
|
+
// A previous session's failed reset does not decide this session's theme.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (this.session?.id !== sessionID || preferenceOwnerID(this.session) !== ownerID) {
|
|
330
|
+
throw new StalePreferenceSessionError();
|
|
331
|
+
}
|
|
332
|
+
this.themePreference = preference;
|
|
333
|
+
this.#applyTheme();
|
|
334
|
+
try {
|
|
335
|
+
if (sessionID === undefined || ownerID === "") {
|
|
336
|
+
await this.client.setPreference("theme", preference);
|
|
337
|
+
} else {
|
|
338
|
+
const result = await getPreferenceWriteQueue().enqueue(
|
|
339
|
+
JSON.stringify([ownerID, "theme"]),
|
|
340
|
+
ownerID,
|
|
341
|
+
() => this.session?.id === sessionID,
|
|
342
|
+
() => this.client.setPreference("theme", preference)
|
|
343
|
+
);
|
|
344
|
+
if (!result.dispatched) {
|
|
345
|
+
await this.#reconcileTheme(themeRequest);
|
|
346
|
+
throw new StalePreferenceSessionError();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (this.session?.id !== sessionID || preferenceOwnerID(this.session) !== ownerID) {
|
|
350
|
+
await this.#reconcileTheme(themeRequest);
|
|
351
|
+
throw new StalePreferenceSessionError();
|
|
352
|
+
}
|
|
353
|
+
this.#confirmedThemePreference = preference;
|
|
354
|
+
this.#confirmedThemeOwnerID = ownerID;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error instanceof StalePreferenceSessionError) throw error;
|
|
357
|
+
if (this.session?.id !== sessionID || preferenceOwnerID(this.session) !== ownerID) {
|
|
358
|
+
await this.#reconcileTheme(themeRequest);
|
|
359
|
+
throw new StalePreferenceSessionError();
|
|
360
|
+
}
|
|
361
|
+
await this.#reconcileTheme(themeRequest);
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async #reconcileTheme(themeRequest: number) {
|
|
367
|
+
if (themeRequest !== this.#themeRequest) return;
|
|
368
|
+
await this.loadTheme();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async resetPreferences(): Promise<void> {
|
|
372
|
+
const sessionID = this.session?.id;
|
|
373
|
+
const ownerID = preferenceOwnerID(this.session);
|
|
374
|
+
const activeReset = this.#preferenceReset;
|
|
375
|
+
if (activeReset !== undefined) {
|
|
376
|
+
if (activeReset.sessionID === sessionID && activeReset.ownerID === ownerID) {
|
|
377
|
+
return activeReset.promise;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
await activeReset.promise;
|
|
381
|
+
} catch {
|
|
382
|
+
// A previous session's failed reset does not decide this session's request.
|
|
383
|
+
}
|
|
384
|
+
if (this.session?.id !== sessionID || preferenceOwnerID(this.session) !== ownerID) {
|
|
385
|
+
throw new Error("The signed-in session changed before preferences could be reset.");
|
|
386
|
+
}
|
|
387
|
+
return this.resetPreferences();
|
|
388
|
+
}
|
|
389
|
+
const themeRequest = ++this.#themeRequest;
|
|
390
|
+
const reset = (async () => {
|
|
391
|
+
try {
|
|
392
|
+
if (ownerID !== "") await getPreferenceWriteQueue().settleOwner(ownerID);
|
|
393
|
+
if (this.session?.id !== sessionID) {
|
|
394
|
+
throw new Error("The signed-in session changed before preferences could be reset.");
|
|
395
|
+
}
|
|
396
|
+
await this.client.resetPreferences();
|
|
397
|
+
if (ownerID !== "") getPreferenceWriteQueue().invalidateOwner(ownerID);
|
|
398
|
+
if (this.session?.id === sessionID) {
|
|
399
|
+
this.#confirmedThemePreference = "system";
|
|
400
|
+
this.#confirmedThemeOwnerID = ownerID;
|
|
401
|
+
this.themePreference = "system";
|
|
402
|
+
this.#applyTheme();
|
|
403
|
+
this.#resetContentLocale();
|
|
404
|
+
await this.refreshAccess(this.contentLocale);
|
|
405
|
+
this.i18n.reset();
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
await this.#reconcileTheme(themeRequest);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
})();
|
|
412
|
+
this.#preferenceReset = { sessionID, ownerID, promise: reset };
|
|
413
|
+
try {
|
|
414
|
+
await reset;
|
|
415
|
+
} finally {
|
|
416
|
+
if (this.#preferenceReset?.promise === reset) this.#preferenceReset = undefined;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#applyTheme() {
|
|
421
|
+
const systemDark =
|
|
422
|
+
typeof window === "undefined" || window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
423
|
+
this.resolvedTheme =
|
|
424
|
+
this.themePreference === "system" ? (systemDark ? "dark" : "light") : this.themePreference;
|
|
425
|
+
if (typeof document !== "undefined") {
|
|
426
|
+
document.documentElement.dataset.theme = this.resolvedTheme;
|
|
427
|
+
document.documentElement.style.colorScheme = this.resolvedTheme;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async bootstrap() {
|
|
432
|
+
const request = ++this.#manifestRequest;
|
|
433
|
+
this.loading = true;
|
|
434
|
+
this.error = undefined;
|
|
435
|
+
try {
|
|
436
|
+
const manifest = await this.client.schema();
|
|
437
|
+
if (request !== this.#manifestRequest) return;
|
|
438
|
+
this.manifest = manifest;
|
|
439
|
+
this.manifestRevision += 1;
|
|
440
|
+
this.#configureContentLocale();
|
|
441
|
+
this.i18n.configure(manifest.application.adminLocalization);
|
|
442
|
+
const admin = manifest.application.admin;
|
|
443
|
+
if (admin !== undefined) {
|
|
444
|
+
const bootstrap = await this.client.authBootstrap(admin.userCollectionSlug);
|
|
445
|
+
if (request !== this.#manifestRequest) return;
|
|
446
|
+
this.authBootstrapAvailable = bootstrap.available;
|
|
447
|
+
if (bootstrap.available) {
|
|
448
|
+
this.session = undefined;
|
|
449
|
+
} else {
|
|
450
|
+
try {
|
|
451
|
+
const session = await this.client.session();
|
|
452
|
+
if (request !== this.#manifestRequest) return;
|
|
453
|
+
this.session = session.collection === admin.userCollectionSlug ? session : undefined;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (request !== this.#manifestRequest) return;
|
|
456
|
+
if (!(error instanceof RiduError) || error.code !== "access_denied") throw error;
|
|
457
|
+
this.session = undefined;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
this.session = undefined;
|
|
462
|
+
this.authBootstrapAvailable = false;
|
|
463
|
+
}
|
|
464
|
+
await this.loadContentLocale(this.#browserRouteContentLocale());
|
|
465
|
+
const accessAuthoritative = await this.refreshAccess(this.contentLocale);
|
|
466
|
+
if (accessAuthoritative && this.session !== undefined && !this.adminAccessAllowed) {
|
|
467
|
+
try {
|
|
468
|
+
await this.client.logout();
|
|
469
|
+
} catch {
|
|
470
|
+
// The local session is still denied even if server cleanup fails.
|
|
471
|
+
}
|
|
472
|
+
this.session = undefined;
|
|
473
|
+
}
|
|
474
|
+
await Promise.all([this.loadTheme(), this.i18n.loadPreferences()]);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (request !== this.#manifestRequest) return;
|
|
477
|
+
this.error = error instanceof Error ? error.message : this.i18n.t("errors:loadAdmin");
|
|
478
|
+
} finally {
|
|
479
|
+
if (request === this.#manifestRequest) this.loading = false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async refreshManifest() {
|
|
484
|
+
const request = ++this.#manifestRequest;
|
|
485
|
+
this.refreshingManifest = true;
|
|
486
|
+
this.manifestRefreshError = undefined;
|
|
487
|
+
try {
|
|
488
|
+
const manifest = await this.client.schema();
|
|
489
|
+
if (request !== this.#manifestRequest) return;
|
|
490
|
+
this.manifest = manifest;
|
|
491
|
+
this.manifestRevision += 1;
|
|
492
|
+
this.#configureContentLocale();
|
|
493
|
+
this.i18n.configure(manifest.application.adminLocalization);
|
|
494
|
+
const admin = manifest.application.admin;
|
|
495
|
+
if (admin === undefined || this.session?.collection !== admin.userCollectionSlug) {
|
|
496
|
+
this.session = undefined;
|
|
497
|
+
}
|
|
498
|
+
if (admin !== undefined && this.session === undefined) {
|
|
499
|
+
const bootstrap = await this.client.authBootstrap(admin.userCollectionSlug);
|
|
500
|
+
if (request !== this.#manifestRequest) return;
|
|
501
|
+
this.authBootstrapAvailable = bootstrap.available;
|
|
502
|
+
} else {
|
|
503
|
+
this.authBootstrapAvailable = false;
|
|
504
|
+
}
|
|
505
|
+
await this.loadContentLocale(this.#browserRouteContentLocale());
|
|
506
|
+
const accessAuthoritative = await this.refreshAccess(this.contentLocale);
|
|
507
|
+
if (accessAuthoritative && this.session !== undefined && !this.adminAccessAllowed) {
|
|
508
|
+
try {
|
|
509
|
+
await this.client.logout();
|
|
510
|
+
} catch {
|
|
511
|
+
// The local session is still denied even if server cleanup fails.
|
|
512
|
+
}
|
|
513
|
+
this.session = undefined;
|
|
514
|
+
}
|
|
515
|
+
await this.i18n.loadPreferences();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (request !== this.#manifestRequest) return;
|
|
518
|
+
this.manifestRefreshError =
|
|
519
|
+
error instanceof Error ? error.message : this.i18n.t("errors:loadAdmin");
|
|
520
|
+
} finally {
|
|
521
|
+
if (request === this.#manifestRequest) this.refreshingManifest = false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async refreshAccess(locale = this.contentLocale): Promise<boolean> {
|
|
526
|
+
const request = ++this.#accessRequest;
|
|
527
|
+
const manifest = this.manifest;
|
|
528
|
+
if (manifest === undefined) {
|
|
529
|
+
this.collectionOperations = {};
|
|
530
|
+
this.globalOperations = {};
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const [collections, globals] = await Promise.all([
|
|
535
|
+
Promise.all(
|
|
536
|
+
manifest.collections.map(async (collection) => {
|
|
537
|
+
const access = await this.client.collectionAccess(collection.slug, { locale });
|
|
538
|
+
return [collection.slug, access.operations] as const;
|
|
539
|
+
})
|
|
540
|
+
),
|
|
541
|
+
Promise.all(
|
|
542
|
+
(manifest.globals ?? []).map(async (global) => {
|
|
543
|
+
const access = await this.client.globalAccess(global.slug, { locale });
|
|
544
|
+
return [global.slug, access.operations] as const;
|
|
545
|
+
})
|
|
546
|
+
),
|
|
547
|
+
]);
|
|
548
|
+
if (request !== this.#accessRequest) return false;
|
|
549
|
+
this.collectionOperations = Object.fromEntries(collections);
|
|
550
|
+
this.globalOperations = Object.fromEntries(globals);
|
|
551
|
+
return true;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (request !== this.#accessRequest) return false;
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#configureContentLocale() {
|
|
559
|
+
const localization = this.manifest?.application.localization;
|
|
560
|
+
if (localization === undefined) {
|
|
561
|
+
this.contentLocale = undefined;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (!localization.locales.some((locale) => locale.code === this.contentLocale)) {
|
|
565
|
+
this.contentLocale = localization.defaultLocale;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#resetContentLocale() {
|
|
570
|
+
this.#contentLocaleRequest += 1;
|
|
571
|
+
const locale = this.manifest?.application.localization?.defaultLocale;
|
|
572
|
+
this.#confirmedContentLocale = locale;
|
|
573
|
+
this.#confirmedContentLocaleOwnerID = preferenceOwnerID(this.session);
|
|
574
|
+
this.contentLocale = locale;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#browserRouteContentLocale() {
|
|
578
|
+
if (typeof window === "undefined") return undefined;
|
|
579
|
+
return this.contentLocaleForPath(
|
|
580
|
+
`${window.location.pathname}${window.location.search}${window.location.hash}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isThemePreference(value: unknown): value is ThemePreference {
|
|
586
|
+
return value === "system" || value === "light" || value === "dark";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function contentLocalePreference(
|
|
590
|
+
value: unknown,
|
|
591
|
+
locales: readonly { code: string }[]
|
|
592
|
+
): string | undefined {
|
|
593
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
594
|
+
const locale = (value as { locale?: unknown }).locale;
|
|
595
|
+
return typeof locale === "string" && locales.some((candidate) => candidate.code === locale)
|
|
596
|
+
? locale
|
|
597
|
+
: undefined;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function contentLocaleFromPath(
|
|
601
|
+
path: string,
|
|
602
|
+
locales: readonly { code: string }[]
|
|
603
|
+
): string | undefined {
|
|
604
|
+
try {
|
|
605
|
+
const origin = "https://ridu.invalid";
|
|
606
|
+
const current = new URL(path, origin);
|
|
607
|
+
if (current.origin !== origin) return undefined;
|
|
608
|
+
const direct = current.searchParams.get("locale");
|
|
609
|
+
if (direct !== null && locales.some((candidate) => candidate.code === direct)) return direct;
|
|
610
|
+
const redirect = current.searchParams.get("redirect");
|
|
611
|
+
if (redirect === null) return undefined;
|
|
612
|
+
const target = new URL(redirect, origin);
|
|
613
|
+
if (target.origin !== origin) return undefined;
|
|
614
|
+
const nested = target.searchParams.get("locale");
|
|
615
|
+
return nested !== null && locales.some((candidate) => candidate.code === nested)
|
|
616
|
+
? nested
|
|
617
|
+
: undefined;
|
|
618
|
+
} catch {
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const [getAdminRuntime, setAdminRuntime] = createContext<AdminRuntime>();
|
|
624
|
+
|
|
625
|
+
export { getAdminRuntime, setAdminRuntime };
|