@momentumcms/admin 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/fesm2022/{momentumcms-admin-array-field.component-ChjP5zK9.mjs → momentumcms-admin-array-field.component-BLL21bqy.mjs} +2 -2
  2. package/fesm2022/{momentumcms-admin-array-field.component-ChjP5zK9.mjs.map → momentumcms-admin-array-field.component-BLL21bqy.mjs.map} +1 -1
  3. package/fesm2022/{momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs → momentumcms-admin-blocks-field.component-BQHIguqN.mjs} +2 -2
  4. package/fesm2022/{momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs.map → momentumcms-admin-blocks-field.component-BQHIguqN.mjs.map} +1 -1
  5. package/fesm2022/{momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs → momentumcms-admin-collapsible-field.component-DSE4X1xV.mjs} +2 -2
  6. package/fesm2022/{momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs.map → momentumcms-admin-collapsible-field.component-DSE4X1xV.mjs.map} +1 -1
  7. package/fesm2022/{momentumcms-admin-global-edit.page-DHr7Icl6.mjs → momentumcms-admin-global-edit.page-CHr9vOxQ.mjs} +2 -2
  8. package/fesm2022/{momentumcms-admin-global-edit.page-DHr7Icl6.mjs.map → momentumcms-admin-global-edit.page-CHr9vOxQ.mjs.map} +1 -1
  9. package/fesm2022/{momentumcms-admin-group-field.component-Bofhumd5.mjs → momentumcms-admin-group-field.component-qy0Z-cRK.mjs} +2 -2
  10. package/fesm2022/{momentumcms-admin-group-field.component-Bofhumd5.mjs.map → momentumcms-admin-group-field.component-qy0Z-cRK.mjs.map} +1 -1
  11. package/fesm2022/{momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs → momentumcms-admin-momentumcms-admin-DDwm1Rm3.mjs} +1112 -448
  12. package/fesm2022/momentumcms-admin-momentumcms-admin-DDwm1Rm3.mjs.map +1 -0
  13. package/fesm2022/{momentumcms-admin-relationship-field.component-DSZhc5MP.mjs → momentumcms-admin-relationship-field.component-DS68G61P.mjs} +2 -2
  14. package/fesm2022/{momentumcms-admin-relationship-field.component-DSZhc5MP.mjs.map → momentumcms-admin-relationship-field.component-DS68G61P.mjs.map} +1 -1
  15. package/fesm2022/{momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs → momentumcms-admin-rich-text-field.component-CZQSu4lc.mjs} +2 -2
  16. package/fesm2022/{momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs.map → momentumcms-admin-rich-text-field.component-CZQSu4lc.mjs.map} +1 -1
  17. package/fesm2022/{momentumcms-admin-row-field.component-DBzqzooT.mjs → momentumcms-admin-row-field.component-109gO-Rp.mjs} +2 -2
  18. package/fesm2022/{momentumcms-admin-row-field.component-DBzqzooT.mjs.map → momentumcms-admin-row-field.component-109gO-Rp.mjs.map} +1 -1
  19. package/fesm2022/{momentumcms-admin-tabs-field.component-BsnCWC5J.mjs → momentumcms-admin-tabs-field.component-DG8vPjj4.mjs} +2 -2
  20. package/fesm2022/{momentumcms-admin-tabs-field.component-BsnCWC5J.mjs.map → momentumcms-admin-tabs-field.component-DG8vPjj4.mjs.map} +1 -1
  21. package/fesm2022/momentumcms-admin.mjs +1 -1
  22. package/package.json +1 -1
  23. package/types/momentumcms-admin.d.ts +229 -5
  24. package/fesm2022/momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs.map +0 -1
@@ -1,15 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, signal, computed, Injectable, PLATFORM_ID, InjectionToken, makeStateKey, TransferState, DestroyRef, effect, input, ChangeDetectionStrategy, Component, output, viewChild, Injector, untracked, runInInjectionContext, afterNextRender, model, forwardRef, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { inject, signal, computed, Injectable, PLATFORM_ID, InjectionToken, makeStateKey, TransferState, DestroyRef, effect, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, input, ChangeDetectionStrategy, Component, output, viewChild, Injector, untracked, runInInjectionContext, afterNextRender, model, forwardRef } from '@angular/core';
3
3
  import { DOCUMENT, isPlatformBrowser, isPlatformServer, NgComponentOutlet, DatePipe } from '@angular/common';
4
4
  import { Router, NavigationEnd, ActivatedRoute, RouterOutlet, RouterLink } from '@angular/router';
5
5
  import { HttpClient, HttpContextToken, HttpResponse, HttpErrorResponse, HttpRequest, HttpEventType, HttpParams } from '@angular/common/http';
6
- import { firstValueFrom, tap, catchError, throwError, Subject, finalize, Observable, of, filter, take } from 'rxjs';
6
+ import { firstValueFrom, tap, catchError, throwError, Subject, finalize, Observable, of, filter, take, combineLatest } from 'rxjs';
7
7
  import { ToastService, ConfirmationService, DIALOG_DATA, Dialog, DialogHeader, DialogTitle, DialogContent, DialogFooter, DialogClose, Button, Badge, Skeleton, DialogService, Card, CardHeader, CardContent, Separator, Progress, CardFooter, Spinner, Alert, Breadcrumbs, BreadcrumbItem, BreadcrumbSeparator, FieldDisplay, Sidebar, SidebarNav, SidebarNavItem, SidebarSection, Avatar, AvatarFallback, DropdownMenu, DropdownMenuItem, DropdownSeparator, DropdownLabel, DropdownTrigger, SidebarService, SidebarTrigger, ToastContainer, Input, McmsFormField, DialogRef, DataTable, Label, Select, CardTitle, CardDescription, Textarea, Pagination, SearchInput, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, Checkbox } from '@momentumcms/ui';
8
8
  import { map } from 'rxjs/operators';
9
9
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
10
10
  import * as i1 from '@angular/cdk/a11y';
11
11
  import { LiveAnnouncer, A11yModule } from '@angular/cdk/a11y';
12
- import { flattenDataFields, humanizeFieldName, isUploadCollection, getSoftDeleteField } from '@momentumcms/core';
12
+ import { flattenDataFields, humanizeFieldName, isUploadCollection, getSoftDeleteField, hasVersionDrafts } from '@momentumcms/core';
13
13
  import { NgIcon, provideIcons } from '@ng-icons/core';
14
14
  import { heroFilm, heroMusicalNote, heroDocumentText, heroArchiveBox, heroPhoto, heroDocument, heroCloudArrowUp, heroXMark, heroCursorArrowRays, heroMap, heroMagnifyingGlass, heroPuzzlePiece, heroCog6Tooth, heroChartBarSquare, heroChevronUpDown, heroBolt, heroFolder, heroUsers, heroNewspaper, heroSquares2x2, heroEye, heroPencilSquare, heroArrowDownTray, heroTrash, heroChevronRight, heroChevronDown, heroChevronUp, heroPlus } from '@ng-icons/heroicons/outline';
15
15
  import { required, validate, applyEach, apply, email, min, max, minLength, maxLength, form, submit } from '@angular/forms/signals';
@@ -947,8 +947,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
947
947
 
948
948
  /**
949
949
  * Route guard that prompts the user before navigating away from a dirty form.
950
+ *
951
+ * Defensive: checks that the component implements HasUnsavedChanges before calling.
952
+ * This is necessary because AdminPageResolver wraps the actual page component.
950
953
  */
951
954
  const unsavedChangesGuard = (component) => {
955
+ if (typeof component.hasUnsavedChanges !== 'function')
956
+ return true;
952
957
  if (!component.hasUnsavedChanges())
953
958
  return true;
954
959
  const feedback = inject(FeedbackService);
@@ -995,6 +1000,7 @@ function momentumAdminRoutes(configOrOptions) {
995
1000
  let branding;
996
1001
  let includeAuthRoutes;
997
1002
  let pluginRoutes;
1003
+ let adminComponents;
998
1004
  let pluginDescriptors;
999
1005
  // Distinguish MomentumConfig (has `db`) from MomentumAdminConfig (has `collections` but no `db` or `basePath`)
1000
1006
  // from MomentumAdminOptions (has `basePath` as required string)
@@ -1010,6 +1016,7 @@ function momentumAdminRoutes(configOrOptions) {
1010
1016
  collections = [...config.collections, ...uniquePluginCollections];
1011
1017
  globals = config.globals;
1012
1018
  branding = config.admin?.branding;
1019
+ adminComponents = config.admin?.components;
1013
1020
  includeAuthRoutes = true;
1014
1021
  pluginRoutes = pluginDescriptors.flatMap((p) => p.adminRoutes ?? []).map(toAdminPluginRoute);
1015
1022
  }
@@ -1024,6 +1031,7 @@ function momentumAdminRoutes(configOrOptions) {
1024
1031
  collections = [...configOrOptions.collections, ...uniquePluginCollections];
1025
1032
  globals = configOrOptions.globals;
1026
1033
  branding = configOrOptions.branding;
1034
+ adminComponents = undefined; // MomentumAdminOptions doesn't carry config-level components
1027
1035
  includeAuthRoutes = configOrOptions.includeAuthRoutes ?? true;
1028
1036
  // Merge explicit plugin routes with plugin-declared admin routes
1029
1037
  const explicitRoutes = configOrOptions.pluginRoutes?.map(toAdminPluginRoute) ?? [];
@@ -1044,6 +1052,7 @@ function momentumAdminRoutes(configOrOptions) {
1044
1052
  collections = [...adminConfig.collections, ...uniquePluginCollections];
1045
1053
  globals = adminConfig.globals;
1046
1054
  branding = adminConfig.admin?.branding;
1055
+ adminComponents = adminConfig.admin?.components;
1047
1056
  includeAuthRoutes = true;
1048
1057
  pluginRoutes = pluginDescriptors.flatMap((p) => p.adminRoutes ?? []).map(toAdminPluginRoute);
1049
1058
  }
@@ -1058,6 +1067,9 @@ function momentumAdminRoutes(configOrOptions) {
1058
1067
  globals,
1059
1068
  branding,
1060
1069
  pluginRoutes,
1070
+ adminComponents,
1071
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- AdminPluginDescriptor is a subset of MomentumPlugin
1072
+ plugins: pluginDescriptors,
1061
1073
  };
1062
1074
  const routes = [];
1063
1075
  // Auth routes (login, setup, password reset) - outside the admin shell
@@ -1095,46 +1107,74 @@ function momentumAdminRoutes(configOrOptions) {
1095
1107
  data: routeData,
1096
1108
  canActivate: [authGuard],
1097
1109
  children: [
1098
- // Dashboard (default route)
1110
+ // Dashboard (default route) — swappable via AdminComponentRegistry
1099
1111
  {
1100
1112
  path: '',
1101
- loadComponent: () => Promise.resolve().then(function () { return dashboard_page; }).then((m) => m.DashboardPage),
1113
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1114
+ data: {
1115
+ adminPageKey: 'dashboard',
1116
+ adminPageFallback: () => Promise.resolve().then(function () { return dashboard_page; }).then((m) => m.DashboardPage),
1117
+ },
1102
1118
  },
1103
- // Media library
1119
+ // Media library — swappable
1104
1120
  {
1105
1121
  path: 'media',
1106
- loadComponent: () => Promise.resolve().then(function () { return mediaLibrary_page; }).then((m) => m.MediaLibraryPage),
1122
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1123
+ data: {
1124
+ adminPageKey: 'media',
1125
+ adminPageFallback: () => Promise.resolve().then(function () { return mediaLibrary_page; }).then((m) => m.MediaLibraryPage),
1126
+ },
1107
1127
  },
1108
- // Collection list
1128
+ // Collection list — swappable (per-collection + global)
1109
1129
  {
1110
1130
  path: 'collections/:slug',
1111
- loadComponent: () => Promise.resolve().then(function () { return collectionList_page; }).then((m) => m.CollectionListPage),
1131
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1132
+ data: {
1133
+ adminPageKey: 'collection-list',
1134
+ adminPageFallback: () => Promise.resolve().then(function () { return collectionList_page; }).then((m) => m.CollectionListPage),
1135
+ },
1112
1136
  canActivate: [collectionAccessGuard],
1113
1137
  },
1114
- // Create new document
1138
+ // Create new document — swappable (per-collection + global)
1115
1139
  {
1116
1140
  path: 'collections/:slug/new',
1117
- loadComponent: () => Promise.resolve().then(function () { return collectionEdit_page; }).then((m) => m.CollectionEditPage),
1141
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1142
+ data: {
1143
+ adminPageKey: 'collection-edit',
1144
+ adminPageFallback: () => Promise.resolve().then(function () { return collectionEdit_page; }).then((m) => m.CollectionEditPage),
1145
+ },
1118
1146
  canActivate: [collectionAccessGuard],
1119
1147
  canDeactivate: [unsavedChangesGuard],
1120
1148
  },
1121
- // View existing document
1149
+ // View existing document — swappable (per-collection + global)
1122
1150
  {
1123
1151
  path: 'collections/:slug/:id',
1124
- loadComponent: () => Promise.resolve().then(function () { return collectionView_page; }).then((m) => m.CollectionViewPage),
1152
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1153
+ data: {
1154
+ adminPageKey: 'collection-view',
1155
+ adminPageFallback: () => Promise.resolve().then(function () { return collectionView_page; }).then((m) => m.CollectionViewPage),
1156
+ },
1125
1157
  canActivate: [collectionAccessGuard],
1126
1158
  },
1127
- // Edit existing document
1159
+ // Edit existing document — swappable (per-collection + global)
1128
1160
  {
1129
1161
  path: 'collections/:slug/:id/edit',
1130
- loadComponent: () => Promise.resolve().then(function () { return collectionEdit_page; }).then((m) => m.CollectionEditPage),
1162
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1163
+ data: {
1164
+ adminPageKey: 'collection-edit',
1165
+ adminPageFallback: () => Promise.resolve().then(function () { return collectionEdit_page; }).then((m) => m.CollectionEditPage),
1166
+ },
1131
1167
  canActivate: [collectionAccessGuard],
1132
1168
  canDeactivate: [unsavedChangesGuard],
1133
1169
  },
1134
- // Global edit
1170
+ // Global edit — swappable
1135
1171
  {
1136
1172
  path: 'globals/:slug',
1137
- loadComponent: () => import('./momentumcms-admin-global-edit.page-DHr7Icl6.mjs').then((m) => m.GlobalEditPage),
1173
+ loadComponent: () => Promise.resolve().then(function () { return adminPageResolver_component; }).then((m) => m.AdminPageResolver),
1174
+ data: {
1175
+ adminPageKey: 'global-edit',
1176
+ adminPageFallback: () => import('./momentumcms-admin-global-edit.page-CHr9vOxQ.mjs').then((m) => m.GlobalEditPage),
1177
+ },
1138
1178
  canDeactivate: [unsavedChangesGuard],
1139
1179
  },
1140
1180
  // Plugin-registered routes
@@ -3185,6 +3225,239 @@ function getPluginRoutesFromRouteData(data) {
3185
3225
  return [];
3186
3226
  }
3187
3227
 
3228
+ /**
3229
+ * Registry for swappable admin page components.
3230
+ *
3231
+ * Maps page keys (e.g., 'dashboard', 'collection-list') to lazy component loaders.
3232
+ * Per-collection overrides use the pattern: 'collections/{slug}/{type}'.
3233
+ *
3234
+ * Resolution chain: per-collection → global → undefined (use built-in default).
3235
+ */
3236
+ class AdminComponentRegistry {
3237
+ components = new Map();
3238
+ /** Register a lazy loader for a page key. Later registrations override earlier ones. */
3239
+ register(key, loader) {
3240
+ this.components.set(key, loader);
3241
+ }
3242
+ /** Get the lazy loader for a page key. Returns undefined if not registered. */
3243
+ get(key) {
3244
+ return this.components.get(key);
3245
+ }
3246
+ /** Check if a page key has a registered component. */
3247
+ has(key) {
3248
+ return this.components.has(key);
3249
+ }
3250
+ /**
3251
+ * Resolve a component loader with per-collection fallback.
3252
+ *
3253
+ * For collection pages, tries `collections/{slug}/{type}` first,
3254
+ * then falls back to the global key (e.g., 'collection-list').
3255
+ */
3256
+ resolve(key, slug) {
3257
+ if (slug) {
3258
+ const type = key.replace('collection-', '');
3259
+ const perCollectionKey = `collections/${slug}/${type}`;
3260
+ const perCollection = this.components.get(perCollectionKey);
3261
+ if (perCollection)
3262
+ return perCollection;
3263
+ }
3264
+ return this.components.get(key);
3265
+ }
3266
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminComponentRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3267
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminComponentRegistry, providedIn: 'root' });
3268
+ }
3269
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminComponentRegistry, decorators: [{
3270
+ type: Injectable,
3271
+ args: [{ providedIn: 'root' }]
3272
+ }] });
3273
+
3274
+ /**
3275
+ * Registry for admin layout slot components.
3276
+ *
3277
+ * Slots are additive — multiple components can be registered for the same slot key.
3278
+ * Per-collection slots use the pattern: `{base-slot}:{collection-slug}`.
3279
+ *
3280
+ * Resolution merges global + per-collection loaders (global first).
3281
+ *
3282
+ * The registry is signal-aware: `resolve()` reads an internal version signal,
3283
+ * so Angular effects that call `resolve()` will re-run when new slots are registered.
3284
+ */
3285
+ class AdminSlotRegistry {
3286
+ slots = new Map();
3287
+ /** Incremented on every register() so signal-based consumers re-evaluate. */
3288
+ _version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : []));
3289
+ /** Register a lazy loader for a slot. Multiple loaders per slot are supported. Duplicate loaders are skipped. */
3290
+ register(slot, loader) {
3291
+ const existing = this.slots.get(slot) ?? [];
3292
+ if (existing.includes(loader))
3293
+ return;
3294
+ existing.push(loader);
3295
+ this.slots.set(slot, existing);
3296
+ this._version.update((v) => v + 1);
3297
+ }
3298
+ /** Get all loaders for a slot key. Returns empty array if none registered. */
3299
+ getAll(slot) {
3300
+ return this.slots.get(slot) ?? [];
3301
+ }
3302
+ /** Check if a slot has any registered loaders. */
3303
+ has(slot) {
3304
+ return this.slots.has(slot);
3305
+ }
3306
+ /**
3307
+ * Resolve slot loaders, merging global and per-collection entries.
3308
+ *
3309
+ * Reads the internal version signal so Angular effects tracking this call
3310
+ * will re-run when new slots are registered.
3311
+ */
3312
+ resolve(slot, slug) {
3313
+ this._version();
3314
+ const global = this.getAll(slot);
3315
+ if (!slug)
3316
+ return global;
3317
+ const perCollection = this.getAll(`${slot}:${slug}`);
3318
+ return [...global, ...perCollection];
3319
+ }
3320
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSlotRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3321
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSlotRegistry, providedIn: 'root' });
3322
+ }
3323
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSlotRegistry, decorators: [{
3324
+ type: Injectable,
3325
+ args: [{ providedIn: 'root' }]
3326
+ }] });
3327
+
3328
+ /**
3329
+ * Register a custom admin page component override.
3330
+ *
3331
+ * ```typescript
3332
+ * export const appConfig: ApplicationConfig = {
3333
+ * providers: [
3334
+ * provideAdminComponent('dashboard', () =>
3335
+ * import('./custom-dashboard.component').then(m => m.CustomDashboard)
3336
+ * ),
3337
+ * ],
3338
+ * };
3339
+ * ```
3340
+ */
3341
+ function provideAdminComponent(key, loader) {
3342
+ return makeEnvironmentProviders([
3343
+ {
3344
+ provide: ENVIRONMENT_INITIALIZER,
3345
+ multi: true,
3346
+ useFactory: () => {
3347
+ const registry = inject(AdminComponentRegistry);
3348
+ return () => {
3349
+ registry.register(key, loader);
3350
+ };
3351
+ },
3352
+ },
3353
+ ]);
3354
+ }
3355
+ /**
3356
+ * Register a component into an admin layout slot.
3357
+ *
3358
+ * ```typescript
3359
+ * export const appConfig: ApplicationConfig = {
3360
+ * providers: [
3361
+ * provideAdminSlot('dashboard:before', () =>
3362
+ * import('./welcome-banner.component').then(m => m.WelcomeBanner)
3363
+ * ),
3364
+ * ],
3365
+ * };
3366
+ * ```
3367
+ */
3368
+ function provideAdminSlot(slot, loader) {
3369
+ return makeEnvironmentProviders([
3370
+ {
3371
+ provide: ENVIRONMENT_INITIALIZER,
3372
+ multi: true,
3373
+ useFactory: () => {
3374
+ const registry = inject(AdminSlotRegistry);
3375
+ return () => {
3376
+ registry.register(slot, loader);
3377
+ };
3378
+ },
3379
+ },
3380
+ ]);
3381
+ }
3382
+ /** Mapping from AdminComponentsConfig page keys to registry keys. */
3383
+ const PAGE_KEY_MAP = {
3384
+ dashboard: 'dashboard',
3385
+ login: 'login',
3386
+ media: 'media',
3387
+ };
3388
+ /** Mapping from AdminComponentsConfig slot keys to registry slot keys. */
3389
+ const GLOBAL_SLOT_MAP = {
3390
+ beforeNavigation: 'shell:nav-start',
3391
+ afterNavigation: 'shell:nav-end',
3392
+ header: 'shell:header',
3393
+ footer: 'shell:footer',
3394
+ beforeDashboard: 'dashboard:before',
3395
+ afterDashboard: 'dashboard:after',
3396
+ beforeLogin: 'login:before',
3397
+ afterLogin: 'login:after',
3398
+ };
3399
+ /** Mapping from CollectionAdminComponentsConfig page keys to page type suffixes. */
3400
+ const COLLECTION_PAGE_MAP = {
3401
+ list: 'list',
3402
+ edit: 'edit',
3403
+ view: 'view',
3404
+ };
3405
+ /** Mapping from CollectionAdminComponentsConfig slot keys to slot key patterns. */
3406
+ const COLLECTION_SLOT_MAP = {
3407
+ beforeList: 'collection-list:before',
3408
+ afterList: 'collection-list:after',
3409
+ beforeEdit: 'collection-edit:before',
3410
+ afterEdit: 'collection-edit:after',
3411
+ editSidebar: 'collection-edit:sidebar',
3412
+ beforeView: 'collection-view:before',
3413
+ afterView: 'collection-view:after',
3414
+ };
3415
+ /**
3416
+ * Read AdminComponentsConfig and CollectionAdminComponentsConfig from config
3417
+ * and register them into AdminComponentRegistry and AdminSlotRegistry.
3418
+ *
3419
+ * Called once in AdminShellComponent.ngOnInit() after route data is available.
3420
+ */
3421
+ function registerConfigComponents(collections, adminComponents, componentRegistry, slotRegistry) {
3422
+ // Register global page overrides and slots from AdminComponentsConfig
3423
+ if (adminComponents) {
3424
+ for (const [configKey, registryKey] of Object.entries(PAGE_KEY_MAP)) {
3425
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Object.entries returns string keys
3426
+ const loader = adminComponents[configKey];
3427
+ if (loader) {
3428
+ componentRegistry.register(registryKey, loader);
3429
+ }
3430
+ }
3431
+ for (const [configKey, slotKey] of Object.entries(GLOBAL_SLOT_MAP)) {
3432
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Object.entries returns string keys
3433
+ const loader = adminComponents[configKey];
3434
+ if (loader) {
3435
+ slotRegistry.register(slotKey, loader);
3436
+ }
3437
+ }
3438
+ }
3439
+ // Register per-collection page overrides and slots
3440
+ for (const collection of collections) {
3441
+ const components = collection.admin?.components;
3442
+ if (!components)
3443
+ continue;
3444
+ for (const [configKey, typeSuffix] of Object.entries(COLLECTION_PAGE_MAP)) {
3445
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Object.entries returns string keys
3446
+ const loader = components[configKey];
3447
+ if (loader) {
3448
+ componentRegistry.register(`collections/${collection.slug}/${typeSuffix}`, loader);
3449
+ }
3450
+ }
3451
+ for (const [configKey, baseSlotKey] of Object.entries(COLLECTION_SLOT_MAP)) {
3452
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Object.entries returns string keys
3453
+ const loader = components[configKey];
3454
+ if (loader) {
3455
+ slotRegistry.register(`${baseSlotKey}:${collection.slug}`, loader);
3456
+ }
3457
+ }
3458
+ }
3459
+ }
3460
+
3188
3461
  /**
3189
3462
  * Entity Form Widget Types
3190
3463
  *
@@ -4229,106 +4502,302 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
4229
4502
  }], ctorParameters: () => [], propDecorators: { collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], documentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentId", required: true }] }], documentLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentLabel", required: false }] }], restored: [{ type: i0.Output, args: ["restored"] }] } });
4230
4503
 
4231
4504
  /**
4232
- * Media Preview Component
4505
+ * Publish Controls Widget
4233
4506
  *
4234
- * Displays a preview of media based on its type:
4235
- * - Images: Thumbnail preview
4236
- * - Videos: Video icon with optional poster
4237
- * - Audio: Audio icon
4238
- * - Documents: Document icon
4239
- * - Other: Generic file icon
4507
+ * Displays the current document status and provides publish/unpublish actions.
4240
4508
  *
4241
4509
  * @example
4242
4510
  * ```html
4243
- * <mcms-media-preview
4244
- * [media]="mediaDocument"
4245
- * [size]="'md'"
4511
+ * <mcms-publish-controls
4512
+ * [collection]="'posts'"
4513
+ * [documentId]="'abc123'"
4514
+ * [documentLabel]="'Post'"
4515
+ * (statusChanged)="onStatusChanged($event)"
4246
4516
  * />
4247
4517
  * ```
4248
4518
  */
4249
- class MediaPreviewComponent {
4250
- /** Media data to preview */
4251
- media = input(null, ...(ngDevMode ? [{ debugName: "media" }] : []));
4252
- /** Size of the preview */
4253
- size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
4254
- /** Custom class override */
4255
- class = input('', ...(ngDevMode ? [{ debugName: "class" }] : []));
4256
- /** Whether to show rounded corners */
4257
- rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : []));
4258
- /** Host classes */
4259
- hostClasses = computed(() => {
4260
- const sizeClass = this.sizeClasses()[this.size()];
4261
- const roundedClass = this.rounded() ? 'rounded-md overflow-hidden' : '';
4262
- return `inline-block ${sizeClass} ${roundedClass} ${this.class()}`;
4263
- }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
4264
- /** Size classes map */
4265
- sizeClasses = computed(() => ({
4266
- xs: 'h-8 w-8',
4267
- sm: 'h-12 w-12',
4268
- md: 'h-20 w-20',
4269
- lg: 'h-32 w-32',
4270
- xl: 'h-48 w-48',
4271
- }), ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
4272
- /** Icon size classes */
4273
- iconClasses = computed(() => {
4274
- const sizes = {
4275
- xs: 'text-lg',
4276
- sm: 'text-xl',
4277
- md: 'text-3xl',
4278
- lg: 'text-4xl',
4279
- xl: 'text-6xl',
4280
- };
4281
- return `${sizes[this.size()]} text-mcms-muted-foreground`;
4282
- }, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
4283
- /** Whether the media is an image */
4284
- isImage = computed(() => {
4285
- const mimeType = this.media()?.mimeType ?? '';
4286
- return mimeType.startsWith('image/');
4287
- }, ...(ngDevMode ? [{ debugName: "isImage" }] : []));
4288
- /** Whether the media is a video */
4289
- isVideo = computed(() => {
4290
- const mimeType = this.media()?.mimeType ?? '';
4291
- return mimeType.startsWith('video/');
4292
- }, ...(ngDevMode ? [{ debugName: "isVideo" }] : []));
4293
- /** Whether the media is audio */
4294
- isAudio = computed(() => {
4295
- const mimeType = this.media()?.mimeType ?? '';
4296
- return mimeType.startsWith('audio/');
4297
- }, ...(ngDevMode ? [{ debugName: "isAudio" }] : []));
4298
- /** Image URL for preview */
4299
- imageUrl = computed(() => {
4300
- const media = this.media();
4301
- if (!media)
4302
- return '';
4303
- return media.url ?? `/api/media/file/${media.path}`;
4304
- }, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
4305
- /** Icon name based on media type */
4306
- iconName = computed(() => {
4307
- const mimeType = this.media()?.mimeType ?? '';
4308
- if (mimeType.startsWith('video/')) {
4309
- return heroFilm;
4310
- }
4311
- if (mimeType.startsWith('audio/')) {
4312
- return heroMusicalNote;
4313
- }
4314
- if (mimeType === 'application/pdf') {
4315
- return heroDocumentText;
4519
+ class PublishControlsWidget {
4520
+ versionService = inject(VersionService);
4521
+ feedback = inject(FeedbackService);
4522
+ /** Collection slug */
4523
+ collection = input.required(...(ngDevMode ? [{ debugName: "collection" }] : []));
4524
+ /** Document ID */
4525
+ documentId = input.required(...(ngDevMode ? [{ debugName: "documentId" }] : []));
4526
+ /** Document label for feedback messages */
4527
+ documentLabel = input('Document', ...(ngDevMode ? [{ debugName: "documentLabel" }] : []));
4528
+ /** Initial status (optional, will be fetched if not provided) */
4529
+ initialStatus = input(undefined, ...(ngDevMode ? [{ debugName: "initialStatus" }] : []));
4530
+ /** Emitted when the status changes */
4531
+ statusChanged = output();
4532
+ /** Current status */
4533
+ status = signal('draft', ...(ngDevMode ? [{ debugName: "status" }] : []));
4534
+ /** Whether a status update is in progress */
4535
+ isUpdating = signal(false, ...(ngDevMode ? [{ debugName: "isUpdating" }] : []));
4536
+ /** Whether status is loading */
4537
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
4538
+ /** Badge variant based on status (derived from status signal) */
4539
+ statusVariant = computed(() => this.status() === 'published' ? 'default' : 'secondary', ...(ngDevMode ? [{ debugName: "statusVariant" }] : []));
4540
+ /** Status label (derived from status signal) */
4541
+ statusLabel = computed(() => (this.status() === 'published' ? 'Published' : 'Draft'), ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
4542
+ constructor() {
4543
+ // Load status when inputs change
4544
+ effect(() => {
4545
+ const collection = this.collection();
4546
+ const docId = this.documentId();
4547
+ const initial = this.initialStatus();
4548
+ if (initial !== undefined) {
4549
+ this.updateStatusDisplay(initial);
4550
+ }
4551
+ else if (collection && docId) {
4552
+ this.loadStatus(collection, docId);
4553
+ }
4554
+ });
4555
+ }
4556
+ /**
4557
+ * Load the current status from the API.
4558
+ */
4559
+ async loadStatus(collection, docId) {
4560
+ this.isLoading.set(true);
4561
+ try {
4562
+ const status = await this.versionService.getStatus(collection, docId);
4563
+ this.updateStatusDisplay(status);
4316
4564
  }
4317
- if (mimeType.startsWith('application/zip') || mimeType.includes('compressed')) {
4318
- return heroArchiveBox;
4565
+ catch {
4566
+ // Default to draft if we can't load status
4567
+ this.updateStatusDisplay('draft');
4319
4568
  }
4320
- if (mimeType.startsWith('image/')) {
4321
- return heroPhoto;
4569
+ finally {
4570
+ this.isLoading.set(false);
4322
4571
  }
4323
- return heroDocument;
4324
- }, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
4325
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MediaPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4326
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MediaPreviewComponent, isStandalone: true, selector: "mcms-media-preview", inputs: { media: { classPropertyName: "media", publicName: "media", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
4327
- @if (isImage()) {
4328
- <img
4329
- [src]="imageUrl()"
4330
- [alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
4331
- class="h-full w-full object-cover"
4572
+ }
4573
+ /**
4574
+ * Update the status display.
4575
+ * statusVariant and statusLabel are computed signals that automatically update.
4576
+ */
4577
+ updateStatusDisplay(status) {
4578
+ this.status.set(status);
4579
+ }
4580
+ /**
4581
+ * Publish the document.
4582
+ */
4583
+ async onPublish() {
4584
+ this.isUpdating.set(true);
4585
+ try {
4586
+ await this.versionService.publish(this.collection(), this.documentId());
4587
+ this.updateStatusDisplay('published');
4588
+ this.statusChanged.emit('published');
4589
+ }
4590
+ catch {
4591
+ // Error handled by crudToastInterceptor
4592
+ }
4593
+ finally {
4594
+ this.isUpdating.set(false);
4595
+ }
4596
+ }
4597
+ /**
4598
+ * Unpublish the document.
4599
+ */
4600
+ async onUnpublish() {
4601
+ const confirmed = await this.feedback.confirmUnpublish(this.documentLabel());
4602
+ if (!confirmed) {
4603
+ return;
4604
+ }
4605
+ this.isUpdating.set(true);
4606
+ try {
4607
+ await this.versionService.unpublish(this.collection(), this.documentId());
4608
+ this.updateStatusDisplay('draft');
4609
+ this.statusChanged.emit('draft');
4610
+ }
4611
+ catch {
4612
+ // Error handled by crudToastInterceptor
4613
+ }
4614
+ finally {
4615
+ this.isUpdating.set(false);
4616
+ }
4617
+ }
4618
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PublishControlsWidget, deps: [], target: i0.ɵɵFactoryTarget.Component });
4619
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PublishControlsWidget, isStandalone: true, selector: "mcms-publish-controls", inputs: { collection: { classPropertyName: "collection", publicName: "collection", isSignal: true, isRequired: true, transformFunction: null }, documentId: { classPropertyName: "documentId", publicName: "documentId", isSignal: true, isRequired: true, transformFunction: null }, documentLabel: { classPropertyName: "documentLabel", publicName: "documentLabel", isSignal: true, isRequired: false, transformFunction: null }, initialStatus: { classPropertyName: "initialStatus", publicName: "initialStatus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { statusChanged: "statusChanged" }, host: { classAttribute: "inline-flex items-center gap-3" }, ngImport: i0, template: `
4620
+ <mcms-badge [variant]="statusVariant()">
4621
+ {{ statusLabel() }}
4622
+ </mcms-badge>
4623
+
4624
+ @if (status() === 'draft') {
4625
+ <button
4626
+ mcms-button
4627
+ variant="primary"
4628
+ size="sm"
4629
+ [disabled]="isUpdating()"
4630
+ (click)="onPublish()"
4631
+ >
4632
+ @if (isUpdating()) {
4633
+ Publishing...
4634
+ } @else {
4635
+ Publish
4636
+ }
4637
+ </button>
4638
+ } @else {
4639
+ <button
4640
+ mcms-button
4641
+ variant="outline"
4642
+ size="sm"
4643
+ [disabled]="isUpdating()"
4644
+ (click)="onUnpublish()"
4645
+ >
4646
+ @if (isUpdating()) {
4647
+ Unpublishing...
4648
+ } @else {
4649
+ Unpublish
4650
+ }
4651
+ </button>
4652
+ }
4653
+ `, isInline: true, dependencies: [{ kind: "component", type: Badge, selector: "mcms-badge", inputs: ["variant", "class", "role", "ariaLabel"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4654
+ }
4655
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PublishControlsWidget, decorators: [{
4656
+ type: Component,
4657
+ args: [{
4658
+ selector: 'mcms-publish-controls',
4659
+ imports: [Badge, Button],
4660
+ changeDetection: ChangeDetectionStrategy.OnPush,
4661
+ host: { class: 'inline-flex items-center gap-3' },
4662
+ template: `
4663
+ <mcms-badge [variant]="statusVariant()">
4664
+ {{ statusLabel() }}
4665
+ </mcms-badge>
4666
+
4667
+ @if (status() === 'draft') {
4668
+ <button
4669
+ mcms-button
4670
+ variant="primary"
4671
+ size="sm"
4672
+ [disabled]="isUpdating()"
4673
+ (click)="onPublish()"
4674
+ >
4675
+ @if (isUpdating()) {
4676
+ Publishing...
4677
+ } @else {
4678
+ Publish
4679
+ }
4680
+ </button>
4681
+ } @else {
4682
+ <button
4683
+ mcms-button
4684
+ variant="outline"
4685
+ size="sm"
4686
+ [disabled]="isUpdating()"
4687
+ (click)="onUnpublish()"
4688
+ >
4689
+ @if (isUpdating()) {
4690
+ Unpublishing...
4691
+ } @else {
4692
+ Unpublish
4693
+ }
4694
+ </button>
4695
+ }
4696
+ `,
4697
+ }]
4698
+ }], ctorParameters: () => [], propDecorators: { collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], documentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentId", required: true }] }], documentLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentLabel", required: false }] }], initialStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialStatus", required: false }] }], statusChanged: [{ type: i0.Output, args: ["statusChanged"] }] } });
4699
+
4700
+ /**
4701
+ * Media Preview Component
4702
+ *
4703
+ * Displays a preview of media based on its type:
4704
+ * - Images: Thumbnail preview
4705
+ * - Videos: Video icon with optional poster
4706
+ * - Audio: Audio icon
4707
+ * - Documents: Document icon
4708
+ * - Other: Generic file icon
4709
+ *
4710
+ * @example
4711
+ * ```html
4712
+ * <mcms-media-preview
4713
+ * [media]="mediaDocument"
4714
+ * [size]="'md'"
4715
+ * />
4716
+ * ```
4717
+ */
4718
+ class MediaPreviewComponent {
4719
+ /** Media data to preview */
4720
+ media = input(null, ...(ngDevMode ? [{ debugName: "media" }] : []));
4721
+ /** Size of the preview */
4722
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
4723
+ /** Custom class override */
4724
+ class = input('', ...(ngDevMode ? [{ debugName: "class" }] : []));
4725
+ /** Whether to show rounded corners */
4726
+ rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : []));
4727
+ /** Host classes */
4728
+ hostClasses = computed(() => {
4729
+ const sizeClass = this.sizeClasses()[this.size()];
4730
+ const roundedClass = this.rounded() ? 'rounded-md overflow-hidden' : '';
4731
+ return `inline-block ${sizeClass} ${roundedClass} ${this.class()}`;
4732
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
4733
+ /** Size classes map */
4734
+ sizeClasses = computed(() => ({
4735
+ xs: 'h-8 w-8',
4736
+ sm: 'h-12 w-12',
4737
+ md: 'h-20 w-20',
4738
+ lg: 'h-32 w-32',
4739
+ xl: 'h-48 w-48',
4740
+ }), ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
4741
+ /** Icon size classes */
4742
+ iconClasses = computed(() => {
4743
+ const sizes = {
4744
+ xs: 'text-lg',
4745
+ sm: 'text-xl',
4746
+ md: 'text-3xl',
4747
+ lg: 'text-4xl',
4748
+ xl: 'text-6xl',
4749
+ };
4750
+ return `${sizes[this.size()]} text-mcms-muted-foreground`;
4751
+ }, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
4752
+ /** Whether the media is an image */
4753
+ isImage = computed(() => {
4754
+ const mimeType = this.media()?.mimeType ?? '';
4755
+ return mimeType.startsWith('image/');
4756
+ }, ...(ngDevMode ? [{ debugName: "isImage" }] : []));
4757
+ /** Whether the media is a video */
4758
+ isVideo = computed(() => {
4759
+ const mimeType = this.media()?.mimeType ?? '';
4760
+ return mimeType.startsWith('video/');
4761
+ }, ...(ngDevMode ? [{ debugName: "isVideo" }] : []));
4762
+ /** Whether the media is audio */
4763
+ isAudio = computed(() => {
4764
+ const mimeType = this.media()?.mimeType ?? '';
4765
+ return mimeType.startsWith('audio/');
4766
+ }, ...(ngDevMode ? [{ debugName: "isAudio" }] : []));
4767
+ /** Image URL for preview */
4768
+ imageUrl = computed(() => {
4769
+ const media = this.media();
4770
+ if (!media)
4771
+ return '';
4772
+ return media.url ?? `/api/media/file/${media.path}`;
4773
+ }, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
4774
+ /** Icon name based on media type */
4775
+ iconName = computed(() => {
4776
+ const mimeType = this.media()?.mimeType ?? '';
4777
+ if (mimeType.startsWith('video/')) {
4778
+ return heroFilm;
4779
+ }
4780
+ if (mimeType.startsWith('audio/')) {
4781
+ return heroMusicalNote;
4782
+ }
4783
+ if (mimeType === 'application/pdf') {
4784
+ return heroDocumentText;
4785
+ }
4786
+ if (mimeType.startsWith('application/zip') || mimeType.includes('compressed')) {
4787
+ return heroArchiveBox;
4788
+ }
4789
+ if (mimeType.startsWith('image/')) {
4790
+ return heroPhoto;
4791
+ }
4792
+ return heroDocument;
4793
+ }, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
4794
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MediaPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4795
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MediaPreviewComponent, isStandalone: true, selector: "mcms-media-preview", inputs: { media: { classPropertyName: "media", publicName: "media", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
4796
+ @if (isImage()) {
4797
+ <img
4798
+ [src]="imageUrl()"
4799
+ [alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
4800
+ class="h-full w-full object-cover"
4332
4801
  />
4333
4802
  } @else {
4334
4803
  <div class="flex h-full w-full items-center justify-center bg-mcms-muted">
@@ -5482,6 +5951,13 @@ class EntityFormWidget {
5482
5951
  }
5483
5952
  return false;
5484
5953
  }, ...(ngDevMode ? [{ debugName: "hasVersioning" }] : []));
5954
+ /** Current document status (from form model or default to 'draft') */
5955
+ documentStatus = computed(() => {
5956
+ const data = this.formModel();
5957
+ if (data['_status'] === 'published')
5958
+ return 'published';
5959
+ return 'draft';
5960
+ }, ...(ngDevMode ? [{ debugName: "documentStatus" }] : []));
5485
5961
  /** Whether draft save is available (edit mode with existing entity) */
5486
5962
  canSaveDraft = computed(() => {
5487
5963
  return this.hasVersioning() && this.mode() === 'edit' && !!this.entityId();
@@ -5845,6 +6321,13 @@ class EntityFormWidget {
5845
6321
  switchToEdit() {
5846
6322
  this.modeChange.emit('edit');
5847
6323
  }
6324
+ /**
6325
+ * Handle status change from publish controls.
6326
+ */
6327
+ onStatusChanged(status) {
6328
+ const data = { ...this.formModel(), _status: status };
6329
+ this.formModel.set(data);
6330
+ }
5848
6331
  /**
5849
6332
  * Handle version restore — reload the entity data.
5850
6333
  */
@@ -5898,17 +6381,28 @@ class EntityFormWidget {
5898
6381
  <div class="mb-8">
5899
6382
  <div class="flex items-start justify-between gap-4">
5900
6383
  <div>
5901
- <h1 class="text-2xl font-semibold tracking-tight">
5902
- @if (isGlobal()) {
5903
- {{ collectionLabelSingular() }}
5904
- } @else if (mode() === 'create') {
5905
- Create {{ collectionLabelSingular() }}
5906
- } @else if (mode() === 'edit') {
5907
- Edit {{ collectionLabelSingular() }}
5908
- } @else {
5909
- View {{ collectionLabelSingular() }}
6384
+ <div class="flex items-center gap-3">
6385
+ <h1 class="text-2xl font-semibold tracking-tight">
6386
+ @if (isGlobal()) {
6387
+ {{ collectionLabelSingular() }}
6388
+ } @else if (mode() === 'create') {
6389
+ Create {{ collectionLabelSingular() }}
6390
+ } @else if (mode() === 'edit') {
6391
+ Edit {{ collectionLabelSingular() }}
6392
+ } @else {
6393
+ View {{ collectionLabelSingular() }}
6394
+ }
6395
+ </h1>
6396
+ @if (hasVersioning() && mode() === 'edit' && entityId()) {
6397
+ <mcms-publish-controls
6398
+ [collection]="collection().slug"
6399
+ [documentId]="entityId() ?? ''"
6400
+ [documentLabel]="collectionLabelSingular()"
6401
+ [initialStatus]="documentStatus()"
6402
+ (statusChanged)="onStatusChanged($event)"
6403
+ />
5910
6404
  }
5911
- </h1>
6405
+ </div>
5912
6406
  <p class="mt-1 text-muted-foreground">
5913
6407
  @if (isGlobal()) {
5914
6408
  Manage {{ collectionLabelSingular().toLowerCase() }} settings.
@@ -6034,14 +6528,14 @@ class EntityFormWidget {
6034
6528
  <div class="mt-8">
6035
6529
  <mcms-version-history
6036
6530
  [collection]="collection().slug"
6037
- [documentId]="entityId()!"
6531
+ [documentId]="entityId() ?? ''"
6038
6532
  [documentLabel]="collectionLabelSingular()"
6039
6533
  (restored)="onVersionRestored()"
6040
6534
  />
6041
6535
  </div>
6042
6536
  }
6043
6537
  </div>
6044
- `, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: FieldRenderer, selector: "mcms-field-renderer", inputs: ["field", "formNode", "formTree", "formModel", "mode", "path"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: CollectionUploadZoneComponent, selector: "mcms-collection-upload-zone", inputs: ["uploadConfig", "disabled", "pendingFile", "isUploading", "uploadProgress", "error", "existingMedia"], outputs: ["fileSelected", "fileRemoved"] }, { kind: "component", type: FocalPointPickerComponent, selector: "mcms-focal-point-picker", inputs: ["imageUrl", "focalPoint", "alt", "naturalWidth", "naturalHeight", "imageSizes"], outputs: ["focalPointChange"] }, { kind: "component", type: ImageVariantsDisplay, selector: "mcms-image-variants-display", inputs: ["sizes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6538
+ `, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: FieldRenderer, selector: "mcms-field-renderer", inputs: ["field", "formNode", "formTree", "formModel", "mode", "path"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: PublishControlsWidget, selector: "mcms-publish-controls", inputs: ["collection", "documentId", "documentLabel", "initialStatus"], outputs: ["statusChanged"] }, { kind: "component", type: CollectionUploadZoneComponent, selector: "mcms-collection-upload-zone", inputs: ["uploadConfig", "disabled", "pendingFile", "isUploading", "uploadProgress", "error", "existingMedia"], outputs: ["fileSelected", "fileRemoved"] }, { kind: "component", type: FocalPointPickerComponent, selector: "mcms-focal-point-picker", inputs: ["imageUrl", "focalPoint", "alt", "naturalWidth", "naturalHeight", "imageSizes"], outputs: ["focalPointChange"] }, { kind: "component", type: ImageVariantsDisplay, selector: "mcms-image-variants-display", inputs: ["sizes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6045
6539
  }
6046
6540
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EntityFormWidget, decorators: [{
6047
6541
  type: Component,
@@ -6059,6 +6553,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
6059
6553
  BreadcrumbItem,
6060
6554
  BreadcrumbSeparator,
6061
6555
  VersionHistoryWidget,
6556
+ PublishControlsWidget,
6062
6557
  CollectionUploadZoneComponent,
6063
6558
  FocalPointPickerComponent,
6064
6559
  ImageVariantsDisplay,
@@ -6083,17 +6578,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
6083
6578
  <div class="mb-8">
6084
6579
  <div class="flex items-start justify-between gap-4">
6085
6580
  <div>
6086
- <h1 class="text-2xl font-semibold tracking-tight">
6087
- @if (isGlobal()) {
6088
- {{ collectionLabelSingular() }}
6089
- } @else if (mode() === 'create') {
6090
- Create {{ collectionLabelSingular() }}
6091
- } @else if (mode() === 'edit') {
6092
- Edit {{ collectionLabelSingular() }}
6093
- } @else {
6094
- View {{ collectionLabelSingular() }}
6581
+ <div class="flex items-center gap-3">
6582
+ <h1 class="text-2xl font-semibold tracking-tight">
6583
+ @if (isGlobal()) {
6584
+ {{ collectionLabelSingular() }}
6585
+ } @else if (mode() === 'create') {
6586
+ Create {{ collectionLabelSingular() }}
6587
+ } @else if (mode() === 'edit') {
6588
+ Edit {{ collectionLabelSingular() }}
6589
+ } @else {
6590
+ View {{ collectionLabelSingular() }}
6591
+ }
6592
+ </h1>
6593
+ @if (hasVersioning() && mode() === 'edit' && entityId()) {
6594
+ <mcms-publish-controls
6595
+ [collection]="collection().slug"
6596
+ [documentId]="entityId() ?? ''"
6597
+ [documentLabel]="collectionLabelSingular()"
6598
+ [initialStatus]="documentStatus()"
6599
+ (statusChanged)="onStatusChanged($event)"
6600
+ />
6095
6601
  }
6096
- </h1>
6602
+ </div>
6097
6603
  <p class="mt-1 text-muted-foreground">
6098
6604
  @if (isGlobal()) {
6099
6605
  Manage {{ collectionLabelSingular().toLowerCase() }} settings.
@@ -6154,277 +6660,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
6154
6660
 
6155
6661
  @if (isUploadCol() && formModelSizes()) {
6156
6662
  <div class="mb-6">
6157
- <mcms-image-variants-display [sizes]="formModelSizes()" />
6158
- </div>
6159
- }
6160
-
6161
- <div class="space-y-6">
6162
- @for (field of visibleFields(); track field.name) {
6163
- <mcms-field-renderer
6164
- [field]="field"
6165
- [formNode]="getFormNode(field.name)"
6166
- [formTree]="entityForm()"
6167
- [formModel]="formModel()"
6168
- [mode]="mode()"
6169
- [path]="field.name"
6170
- />
6171
- }
6172
- </div>
6173
- }
6174
- </mcms-card-content>
6175
-
6176
- <mcms-card-footer class="flex justify-end gap-3 border-t bg-muted/50 px-6 py-4">
6177
- @if (mode() !== 'view') {
6178
- <button
6179
- mcms-button
6180
- variant="outline"
6181
- [disabled]="isSubmitting() || isSavingDraft()"
6182
- (click)="onCancel()"
6183
- >
6184
- Cancel
6185
- </button>
6186
- @if (canSaveDraft()) {
6187
- <button
6188
- mcms-button
6189
- variant="outline"
6190
- [disabled]="isSubmitting() || isSavingDraft()"
6191
- (click)="onSaveDraft()"
6192
- >
6193
- @if (isSavingDraft()) {
6194
- <mcms-spinner size="sm" class="mr-2" />
6195
- }
6196
- Save Draft
6197
- </button>
6198
- }
6199
- <button
6200
- mcms-button
6201
- variant="primary"
6202
- [disabled]="isSubmitting() || isSavingDraft()"
6203
- (click)="onSubmit()"
6204
- >
6205
- @if (isSubmitting()) {
6206
- <mcms-spinner size="sm" class="mr-2" />
6207
- }
6208
- {{ mode() === 'create' ? 'Create' : 'Save Changes' }}
6209
- </button>
6210
- } @else {
6211
- @if (canEdit()) {
6212
- <button mcms-button variant="primary" (click)="switchToEdit()">Edit</button>
6213
- }
6214
- }
6215
- </mcms-card-footer>
6216
- </mcms-card>
6217
-
6218
- @if (hasVersioning() && mode() === 'edit' && entityId()) {
6219
- <div class="mt-8">
6220
- <mcms-version-history
6221
- [collection]="collection().slug"
6222
- [documentId]="entityId()!"
6223
- [documentLabel]="collectionLabelSingular()"
6224
- (restored)="onVersionRestored()"
6225
- />
6226
- </div>
6227
- }
6228
- </div>
6229
- `,
6230
- }]
6231
- }], ctorParameters: () => [], propDecorators: { collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], basePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "basePath", required: false }] }], showBreadcrumbs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBreadcrumbs", required: false }] }], suppressNavigation: [{ type: i0.Input, args: [{ isSignal: true, alias: "suppressNavigation", required: false }] }], isGlobal: [{ type: i0.Input, args: [{ isSignal: true, alias: "isGlobal", required: false }] }], globalSlug: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalSlug", required: false }] }], saved: [{ type: i0.Output, args: ["saved"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }], saveError: [{ type: i0.Output, args: ["saveError"] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }], draftSaved: [{ type: i0.Output, args: ["draftSaved"] }] } });
6232
-
6233
- /**
6234
- * Publish Controls Widget
6235
- *
6236
- * Displays the current document status and provides publish/unpublish actions.
6237
- *
6238
- * @example
6239
- * ```html
6240
- * <mcms-publish-controls
6241
- * [collection]="'posts'"
6242
- * [documentId]="'abc123'"
6243
- * [documentLabel]="'Post'"
6244
- * (statusChanged)="onStatusChanged($event)"
6245
- * />
6246
- * ```
6247
- */
6248
- class PublishControlsWidget {
6249
- versionService = inject(VersionService);
6250
- feedback = inject(FeedbackService);
6251
- /** Collection slug */
6252
- collection = input.required(...(ngDevMode ? [{ debugName: "collection" }] : []));
6253
- /** Document ID */
6254
- documentId = input.required(...(ngDevMode ? [{ debugName: "documentId" }] : []));
6255
- /** Document label for feedback messages */
6256
- documentLabel = input('Document', ...(ngDevMode ? [{ debugName: "documentLabel" }] : []));
6257
- /** Initial status (optional, will be fetched if not provided) */
6258
- initialStatus = input(undefined, ...(ngDevMode ? [{ debugName: "initialStatus" }] : []));
6259
- /** Emitted when the status changes */
6260
- statusChanged = output();
6261
- /** Current status */
6262
- status = signal('draft', ...(ngDevMode ? [{ debugName: "status" }] : []));
6263
- /** Whether a status update is in progress */
6264
- isUpdating = signal(false, ...(ngDevMode ? [{ debugName: "isUpdating" }] : []));
6265
- /** Whether status is loading */
6266
- isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6267
- /** Badge variant based on status (derived from status signal) */
6268
- statusVariant = computed(() => this.status() === 'published' ? 'default' : 'secondary', ...(ngDevMode ? [{ debugName: "statusVariant" }] : []));
6269
- /** Status label (derived from status signal) */
6270
- statusLabel = computed(() => (this.status() === 'published' ? 'Published' : 'Draft'), ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
6271
- constructor() {
6272
- // Load status when inputs change
6273
- effect(() => {
6274
- const collection = this.collection();
6275
- const docId = this.documentId();
6276
- const initial = this.initialStatus();
6277
- if (initial !== undefined) {
6278
- this.updateStatusDisplay(initial);
6279
- }
6280
- else if (collection && docId) {
6281
- this.loadStatus(collection, docId);
6282
- }
6283
- });
6284
- }
6285
- /**
6286
- * Load the current status from the API.
6287
- */
6288
- async loadStatus(collection, docId) {
6289
- this.isLoading.set(true);
6290
- try {
6291
- const status = await this.versionService.getStatus(collection, docId);
6292
- this.updateStatusDisplay(status);
6293
- }
6294
- catch {
6295
- // Default to draft if we can't load status
6296
- this.updateStatusDisplay('draft');
6297
- }
6298
- finally {
6299
- this.isLoading.set(false);
6300
- }
6301
- }
6302
- /**
6303
- * Update the status display.
6304
- * statusVariant and statusLabel are computed signals that automatically update.
6305
- */
6306
- updateStatusDisplay(status) {
6307
- this.status.set(status);
6308
- }
6309
- /**
6310
- * Publish the document.
6311
- */
6312
- async onPublish() {
6313
- this.isUpdating.set(true);
6314
- try {
6315
- await this.versionService.publish(this.collection(), this.documentId());
6316
- this.updateStatusDisplay('published');
6317
- this.statusChanged.emit('published');
6318
- }
6319
- catch {
6320
- // Error handled by crudToastInterceptor
6321
- }
6322
- finally {
6323
- this.isUpdating.set(false);
6324
- }
6325
- }
6326
- /**
6327
- * Unpublish the document.
6328
- */
6329
- async onUnpublish() {
6330
- const confirmed = await this.feedback.confirmUnpublish(this.documentLabel());
6331
- if (!confirmed) {
6332
- return;
6333
- }
6334
- this.isUpdating.set(true);
6335
- try {
6336
- await this.versionService.unpublish(this.collection(), this.documentId());
6337
- this.updateStatusDisplay('draft');
6338
- this.statusChanged.emit('draft');
6339
- }
6340
- catch {
6341
- // Error handled by crudToastInterceptor
6342
- }
6343
- finally {
6344
- this.isUpdating.set(false);
6345
- }
6346
- }
6347
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PublishControlsWidget, deps: [], target: i0.ɵɵFactoryTarget.Component });
6348
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PublishControlsWidget, isStandalone: true, selector: "mcms-publish-controls", inputs: { collection: { classPropertyName: "collection", publicName: "collection", isSignal: true, isRequired: true, transformFunction: null }, documentId: { classPropertyName: "documentId", publicName: "documentId", isSignal: true, isRequired: true, transformFunction: null }, documentLabel: { classPropertyName: "documentLabel", publicName: "documentLabel", isSignal: true, isRequired: false, transformFunction: null }, initialStatus: { classPropertyName: "initialStatus", publicName: "initialStatus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { statusChanged: "statusChanged" }, host: { classAttribute: "inline-flex items-center gap-3" }, ngImport: i0, template: `
6349
- <mcms-badge [variant]="statusVariant()">
6350
- {{ statusLabel() }}
6351
- </mcms-badge>
6352
-
6353
- @if (status() === 'draft') {
6354
- <button
6355
- mcms-button
6356
- variant="primary"
6357
- size="sm"
6358
- [disabled]="isUpdating()"
6359
- (click)="onPublish()"
6360
- >
6361
- @if (isUpdating()) {
6362
- Publishing...
6363
- } @else {
6364
- Publish
6365
- }
6366
- </button>
6367
- } @else {
6368
- <button
6369
- mcms-button
6370
- variant="outline"
6371
- size="sm"
6372
- [disabled]="isUpdating()"
6373
- (click)="onUnpublish()"
6374
- >
6375
- @if (isUpdating()) {
6376
- Unpublishing...
6377
- } @else {
6378
- Unpublish
6379
- }
6380
- </button>
6381
- }
6382
- `, isInline: true, dependencies: [{ kind: "component", type: Badge, selector: "mcms-badge", inputs: ["variant", "class", "role", "ariaLabel"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6383
- }
6384
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PublishControlsWidget, decorators: [{
6385
- type: Component,
6386
- args: [{
6387
- selector: 'mcms-publish-controls',
6388
- imports: [Badge, Button],
6389
- changeDetection: ChangeDetectionStrategy.OnPush,
6390
- host: { class: 'inline-flex items-center gap-3' },
6391
- template: `
6392
- <mcms-badge [variant]="statusVariant()">
6393
- {{ statusLabel() }}
6394
- </mcms-badge>
6663
+ <mcms-image-variants-display [sizes]="formModelSizes()" />
6664
+ </div>
6665
+ }
6395
6666
 
6396
- @if (status() === 'draft') {
6397
- <button
6398
- mcms-button
6399
- variant="primary"
6400
- size="sm"
6401
- [disabled]="isUpdating()"
6402
- (click)="onPublish()"
6403
- >
6404
- @if (isUpdating()) {
6405
- Publishing...
6406
- } @else {
6407
- Publish
6408
- }
6409
- </button>
6410
- } @else {
6411
- <button
6412
- mcms-button
6413
- variant="outline"
6414
- size="sm"
6415
- [disabled]="isUpdating()"
6416
- (click)="onUnpublish()"
6417
- >
6418
- @if (isUpdating()) {
6419
- Unpublishing...
6420
- } @else {
6421
- Unpublish
6422
- }
6423
- </button>
6424
- }
6667
+ <div class="space-y-6">
6668
+ @for (field of visibleFields(); track field.name) {
6669
+ <mcms-field-renderer
6670
+ [field]="field"
6671
+ [formNode]="getFormNode(field.name)"
6672
+ [formTree]="entityForm()"
6673
+ [formModel]="formModel()"
6674
+ [mode]="mode()"
6675
+ [path]="field.name"
6676
+ />
6677
+ }
6678
+ </div>
6679
+ }
6680
+ </mcms-card-content>
6681
+
6682
+ <mcms-card-footer class="flex justify-end gap-3 border-t bg-muted/50 px-6 py-4">
6683
+ @if (mode() !== 'view') {
6684
+ <button
6685
+ mcms-button
6686
+ variant="outline"
6687
+ [disabled]="isSubmitting() || isSavingDraft()"
6688
+ (click)="onCancel()"
6689
+ >
6690
+ Cancel
6691
+ </button>
6692
+ @if (canSaveDraft()) {
6693
+ <button
6694
+ mcms-button
6695
+ variant="outline"
6696
+ [disabled]="isSubmitting() || isSavingDraft()"
6697
+ (click)="onSaveDraft()"
6698
+ >
6699
+ @if (isSavingDraft()) {
6700
+ <mcms-spinner size="sm" class="mr-2" />
6701
+ }
6702
+ Save Draft
6703
+ </button>
6704
+ }
6705
+ <button
6706
+ mcms-button
6707
+ variant="primary"
6708
+ [disabled]="isSubmitting() || isSavingDraft()"
6709
+ (click)="onSubmit()"
6710
+ >
6711
+ @if (isSubmitting()) {
6712
+ <mcms-spinner size="sm" class="mr-2" />
6713
+ }
6714
+ {{ mode() === 'create' ? 'Create' : 'Save Changes' }}
6715
+ </button>
6716
+ } @else {
6717
+ @if (canEdit()) {
6718
+ <button mcms-button variant="primary" (click)="switchToEdit()">Edit</button>
6719
+ }
6720
+ }
6721
+ </mcms-card-footer>
6722
+ </mcms-card>
6723
+
6724
+ @if (hasVersioning() && mode() === 'edit' && entityId()) {
6725
+ <div class="mt-8">
6726
+ <mcms-version-history
6727
+ [collection]="collection().slug"
6728
+ [documentId]="entityId() ?? ''"
6729
+ [documentLabel]="collectionLabelSingular()"
6730
+ (restored)="onVersionRestored()"
6731
+ />
6732
+ </div>
6733
+ }
6734
+ </div>
6425
6735
  `,
6426
6736
  }]
6427
- }], ctorParameters: () => [], propDecorators: { collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], documentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentId", required: true }] }], documentLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentLabel", required: false }] }], initialStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialStatus", required: false }] }], statusChanged: [{ type: i0.Output, args: ["statusChanged"] }] } });
6737
+ }], ctorParameters: () => [], propDecorators: { collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], basePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "basePath", required: false }] }], showBreadcrumbs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBreadcrumbs", required: false }] }], suppressNavigation: [{ type: i0.Input, args: [{ isSignal: true, alias: "suppressNavigation", required: false }] }], isGlobal: [{ type: i0.Input, args: [{ isSignal: true, alias: "isGlobal", required: false }] }], globalSlug: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalSlug", required: false }] }], saved: [{ type: i0.Output, args: ["saved"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }], saveError: [{ type: i0.Output, args: ["saveError"] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }], draftSaved: [{ type: i0.Output, args: ["draftSaved"] }] } });
6428
6738
 
6429
6739
  /**
6430
6740
  * Entity View Widget
@@ -7530,6 +7840,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
7530
7840
  }]
7531
7841
  }] });
7532
7842
 
7843
+ /**
7844
+ * Renders all components registered for a named admin layout slot.
7845
+ *
7846
+ * Usage:
7847
+ * ```html
7848
+ * <mcms-admin-slot slot="dashboard:before" />
7849
+ * <mcms-admin-slot slot="collection-list:before" [collectionSlug]="slug" />
7850
+ * ```
7851
+ */
7852
+ class AdminSlotOutlet {
7853
+ registry = inject(AdminSlotRegistry);
7854
+ /** The slot key to render (e.g., 'dashboard:before'). */
7855
+ slot = input.required(...(ngDevMode ? [{ debugName: "slot" }] : []));
7856
+ /** Optional collection slug for per-collection slot resolution. */
7857
+ collectionSlug = input(...(ngDevMode ? [undefined, { debugName: "collectionSlug" }] : []));
7858
+ /** Optional context passed as inputs to slot components. */
7859
+ context = input({}, ...(ngDevMode ? [{ debugName: "context" }] : []));
7860
+ /** Resolved component types after lazy loading. */
7861
+ resolvedComponents = signal([], ...(ngDevMode ? [{ debugName: "resolvedComponents" }] : []));
7862
+ /** Inputs to pass to each slot component. */
7863
+ slotInputs = computed(() => ({
7864
+ ...this.context(),
7865
+ }), ...(ngDevMode ? [{ debugName: "slotInputs" }] : []));
7866
+ /** Incremented on every effect run to detect stale promise resolutions. */
7867
+ loadGeneration = 0;
7868
+ constructor() {
7869
+ effect(() => {
7870
+ const slot = this.slot();
7871
+ const slug = this.collectionSlug();
7872
+ const loaders = this.registry.resolve(slot, slug);
7873
+ if (loaders.length === 0) {
7874
+ this.resolvedComponents.set([]);
7875
+ return;
7876
+ }
7877
+ const generation = ++this.loadGeneration;
7878
+ Promise.all(loaders.map((loader) => loader()))
7879
+ .then((components) => {
7880
+ if (generation !== this.loadGeneration)
7881
+ return;
7882
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- loaders resolve to unknown from registry, safe cast to Type[]
7883
+ this.resolvedComponents.set(components);
7884
+ })
7885
+ .catch((err) => {
7886
+ if (generation !== this.loadGeneration)
7887
+ return;
7888
+ console.error('[AdminSlotOutlet] Failed to load slot components:', err);
7889
+ this.resolvedComponents.set([]);
7890
+ });
7891
+ });
7892
+ }
7893
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSlotOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
7894
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AdminSlotOutlet, isStandalone: true, selector: "mcms-admin-slot", inputs: { slot: { classPropertyName: "slot", publicName: "slot", isSignal: true, isRequired: true, transformFunction: null }, collectionSlug: { classPropertyName: "collectionSlug", publicName: "collectionSlug", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
7895
+ @for (component of resolvedComponents(); track $index) {
7896
+ <ng-container *ngComponentOutlet="component; inputs: slotInputs()" />
7897
+ }
7898
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7899
+ }
7900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSlotOutlet, decorators: [{
7901
+ type: Component,
7902
+ args: [{
7903
+ selector: 'mcms-admin-slot',
7904
+ imports: [NgComponentOutlet],
7905
+ changeDetection: ChangeDetectionStrategy.OnPush,
7906
+ host: { class: 'block' },
7907
+ template: `
7908
+ @for (component of resolvedComponents(); track $index) {
7909
+ <ng-container *ngComponentOutlet="component; inputs: slotInputs()" />
7910
+ }
7911
+ `,
7912
+ }]
7913
+ }], ctorParameters: () => [], propDecorators: { slot: [{ type: i0.Input, args: [{ isSignal: true, alias: "slot", required: true }] }], collectionSlug: [{ type: i0.Input, args: [{ isSignal: true, alias: "collectionSlug", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }] } });
7914
+
7533
7915
  const DEFAULT_GROUP = 'Collections';
7534
7916
  /**
7535
7917
  * Slugify a group name into a valid HTML id attribute value.
@@ -7772,6 +8154,8 @@ class AdminSidebarWidget {
7772
8154
  [exact]="true"
7773
8155
  />
7774
8156
 
8157
+ <mcms-admin-slot slot="shell:nav-start" />
8158
+
7775
8159
  <!-- Collection Sections (grouped by admin.group) -->
7776
8160
  @for (group of collectionGroups(); track group.id) {
7777
8161
  <mcms-sidebar-section [title]="group.name">
@@ -7816,6 +8200,8 @@ class AdminSidebarWidget {
7816
8200
  }
7817
8201
  </mcms-sidebar-section>
7818
8202
  }
8203
+
8204
+ <mcms-admin-slot slot="shell:nav-end" />
7819
8205
  </mcms-sidebar-nav>
7820
8206
  </div>
7821
8207
 
@@ -7863,7 +8249,7 @@ class AdminSidebarWidget {
7863
8249
  }
7864
8250
  </div>
7865
8251
  </mcms-sidebar>
7866
- `, isInline: true, dependencies: [{ kind: "component", type: Sidebar, selector: "mcms-sidebar", inputs: ["width", "collapsedWidth", "collapsed", "class"] }, { kind: "component", type: SidebarNav, selector: "mcms-sidebar-nav", inputs: ["ariaLabel", "class"] }, { kind: "component", type: SidebarNavItem, selector: "mcms-sidebar-nav-item", inputs: ["label", "href", "icon", "badge", "active", "disabled", "exact", "class"], outputs: ["clicked"] }, { kind: "component", type: SidebarSection, selector: "mcms-sidebar-section", inputs: ["title", "collapsible", "expanded", "class"], outputs: ["expandedChange"] }, { kind: "component", type: Avatar, selector: "mcms-avatar", inputs: ["size", "class", "ariaLabel"] }, { kind: "component", type: AvatarFallback, selector: "mcms-avatar-fallback", inputs: ["delayMs", "class"] }, { kind: "component", type: DropdownMenu, selector: "mcms-dropdown-menu", inputs: ["disabled", "wrap", "typeaheadDelay", "class"], outputs: ["itemSelected"] }, { kind: "component", type: DropdownMenuItem, selector: "button[mcms-dropdown-item], a[mcms-dropdown-item]", inputs: ["value", "disabled", "shortcut", "class"], outputs: ["selected"] }, { kind: "component", type: DropdownSeparator, selector: "mcms-dropdown-separator" }, { kind: "component", type: DropdownLabel, selector: "mcms-dropdown-label" }, { kind: "directive", type: DropdownTrigger, selector: "[mcmsDropdownTrigger]", inputs: ["mcmsDropdownTrigger", "dropdownSide", "dropdownAlign", "dropdownOffset", "dropdownDisabled"], outputs: ["opened", "closed"], exportAs: ["mcmsDropdownTrigger"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8252
+ `, isInline: true, dependencies: [{ kind: "component", type: Sidebar, selector: "mcms-sidebar", inputs: ["width", "collapsedWidth", "collapsed", "class"] }, { kind: "component", type: SidebarNav, selector: "mcms-sidebar-nav", inputs: ["ariaLabel", "class"] }, { kind: "component", type: SidebarNavItem, selector: "mcms-sidebar-nav-item", inputs: ["label", "href", "icon", "badge", "active", "disabled", "exact", "class"], outputs: ["clicked"] }, { kind: "component", type: SidebarSection, selector: "mcms-sidebar-section", inputs: ["title", "collapsible", "expanded", "class"], outputs: ["expandedChange"] }, { kind: "component", type: Avatar, selector: "mcms-avatar", inputs: ["size", "class", "ariaLabel"] }, { kind: "component", type: AvatarFallback, selector: "mcms-avatar-fallback", inputs: ["delayMs", "class"] }, { kind: "component", type: DropdownMenu, selector: "mcms-dropdown-menu", inputs: ["disabled", "wrap", "typeaheadDelay", "class"], outputs: ["itemSelected"] }, { kind: "component", type: DropdownMenuItem, selector: "button[mcms-dropdown-item], a[mcms-dropdown-item]", inputs: ["value", "disabled", "shortcut", "class"], outputs: ["selected"] }, { kind: "component", type: DropdownSeparator, selector: "mcms-dropdown-separator" }, { kind: "component", type: DropdownLabel, selector: "mcms-dropdown-label" }, { kind: "directive", type: DropdownTrigger, selector: "[mcmsDropdownTrigger]", inputs: ["mcmsDropdownTrigger", "dropdownSide", "dropdownAlign", "dropdownOffset", "dropdownDisabled"], outputs: ["opened", "closed"], exportAs: ["mcmsDropdownTrigger"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7867
8253
  }
7868
8254
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminSidebarWidget, decorators: [{
7869
8255
  type: Component,
@@ -7882,6 +8268,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
7882
8268
  DropdownLabel,
7883
8269
  DropdownTrigger,
7884
8270
  NgIcon,
8271
+ AdminSlotOutlet,
7885
8272
  ],
7886
8273
  providers: [
7887
8274
  provideIcons({
@@ -7947,6 +8334,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
7947
8334
  [exact]="true"
7948
8335
  />
7949
8336
 
8337
+ <mcms-admin-slot slot="shell:nav-start" />
8338
+
7950
8339
  <!-- Collection Sections (grouped by admin.group) -->
7951
8340
  @for (group of collectionGroups(); track group.id) {
7952
8341
  <mcms-sidebar-section [title]="group.name">
@@ -7991,6 +8380,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
7991
8380
  }
7992
8381
  </mcms-sidebar-section>
7993
8382
  }
8383
+
8384
+ <mcms-admin-slot slot="shell:nav-end" />
7994
8385
  </mcms-sidebar-nav>
7995
8386
  </div>
7996
8387
 
@@ -8061,6 +8452,8 @@ class AdminShellComponent {
8061
8452
  auth = inject(MomentumAuthService);
8062
8453
  collectionAccess = inject(CollectionAccessService);
8063
8454
  sidebar = inject(SidebarService);
8455
+ componentRegistry = inject(AdminComponentRegistry);
8456
+ slotRegistry = inject(AdminSlotRegistry);
8064
8457
  entitySheet = inject(EntitySheetService);
8065
8458
  /** All collections from route data */
8066
8459
  allCollections = computed(() => {
@@ -8110,6 +8503,23 @@ class AdminShellComponent {
8110
8503
  };
8111
8504
  }, ...(ngDevMode ? [{ debugName: "sidebarUser" }] : []));
8112
8505
  ngOnInit() {
8506
+ // Register config-level component overrides and slot registrations.
8507
+ // This reads from route data (which has the full MomentumConfig-derived data)
8508
+ // and bridges config declarations into the runtime registries.
8509
+ const routeData = this.route.snapshot.data;
8510
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- route data is Record<string, unknown>
8511
+ const adminComponents = routeData['adminComponents'];
8512
+ registerConfigComponents(this.allCollections(), adminComponents, this.componentRegistry, this.slotRegistry);
8513
+ // Also register plugin-declared admin components
8514
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- route data is Record<string, unknown>
8515
+ const plugins = routeData['plugins'];
8516
+ if (plugins) {
8517
+ for (const plugin of plugins) {
8518
+ if (plugin.adminComponents) {
8519
+ registerConfigComponents([], plugin.adminComponents, this.componentRegistry, this.slotRegistry);
8520
+ }
8521
+ }
8522
+ }
8113
8523
  // Keyboard shortcuts, auth, and sheet restoration only run in the browser.
8114
8524
  // SSR user is provided via MOMENTUM_API_CONTEXT (used by injectUser above).
8115
8525
  if (!isPlatformBrowser(this.platformId)) {
@@ -8185,9 +8595,11 @@ class AdminShellComponent {
8185
8595
 
8186
8596
  <!-- Main Content (with top padding on mobile for header, normal padding at md+) -->
8187
8597
  <main id="mcms-main-content" class="flex-1 p-8 overflow-y-auto overflow-x-hidden pt-20 md:pt-8">
8598
+ <mcms-admin-slot slot="shell:header" />
8188
8599
  @defer (hydrate on immediate) {
8189
8600
  <router-outlet />
8190
8601
  }
8602
+ <mcms-admin-slot slot="shell:footer" />
8191
8603
  </main>
8192
8604
 
8193
8605
  <mcms-toast-container />
@@ -8222,7 +8634,7 @@ class AdminShellComponent {
8222
8634
  </div>
8223
8635
  </div>
8224
8636
  }
8225
- `, isInline: true, styles: ["@keyframes mcms-fade-in{0%{opacity:0}to{opacity:1}}@keyframes mcms-fade-out{0%{opacity:1}to{opacity:0}}@keyframes mcms-slide-in-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes mcms-slide-out-right{0%{transform:translate(0)}to{transform:translate(100%)}}.sheet-backdrop{animation:mcms-fade-in .15s ease-out}.sheet-backdrop-closing{animation:mcms-fade-out .15s ease-in forwards}.sheet-panel{animation:mcms-slide-in-right .2s ease-out}.sheet-panel-closing{animation:mcms-slide-out-right .2s ease-in forwards}\n"], dependencies: [{ kind: "component", type: AdminSidebarWidget, selector: "mcms-admin-sidebar", inputs: ["branding", "collections", "globals", "pluginRoutes", "user", "basePath", "collapsed", "width"], outputs: ["signOut"] }, { kind: "component", type: SidebarTrigger, selector: "mcms-sidebar-trigger", inputs: ["class"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: EntitySheetContentComponent, selector: "mcms-entity-sheet-content" }, { kind: "component", type: ToastContainer, selector: "mcms-toast-container" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [RouterOutlet]] });
8637
+ `, isInline: true, styles: ["@keyframes mcms-fade-in{0%{opacity:0}to{opacity:1}}@keyframes mcms-fade-out{0%{opacity:1}to{opacity:0}}@keyframes mcms-slide-in-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes mcms-slide-out-right{0%{transform:translate(0)}to{transform:translate(100%)}}.sheet-backdrop{animation:mcms-fade-in .15s ease-out}.sheet-backdrop-closing{animation:mcms-fade-out .15s ease-in forwards}.sheet-panel{animation:mcms-slide-in-right .2s ease-out}.sheet-panel-closing{animation:mcms-slide-out-right .2s ease-in forwards}\n"], dependencies: [{ kind: "component", type: AdminSidebarWidget, selector: "mcms-admin-sidebar", inputs: ["branding", "collections", "globals", "pluginRoutes", "user", "basePath", "collapsed", "width"], outputs: ["signOut"] }, { kind: "component", type: SidebarTrigger, selector: "mcms-sidebar-trigger", inputs: ["class"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: EntitySheetContentComponent, selector: "mcms-entity-sheet-content" }, { kind: "component", type: ToastContainer, selector: "mcms-toast-container" }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [RouterOutlet]] });
8226
8638
  }
8227
8639
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminShellComponent, decorators: [{
8228
8640
  type: Component,
@@ -8233,6 +8645,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
8233
8645
  A11yModule,
8234
8646
  EntitySheetContentComponent,
8235
8647
  ToastContainer,
8648
+ AdminSlotOutlet,
8236
8649
  ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
8237
8650
  class: 'flex h-screen overflow-hidden bg-background',
8238
8651
  '(document:keydown.escape)': 'onEscapeKey()',
@@ -8265,9 +8678,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
8265
8678
 
8266
8679
  <!-- Main Content (with top padding on mobile for header, normal padding at md+) -->
8267
8680
  <main id="mcms-main-content" class="flex-1 p-8 overflow-y-auto overflow-x-hidden pt-20 md:pt-8">
8681
+ <mcms-admin-slot slot="shell:header" />
8268
8682
  @defer (hydrate on immediate) {
8269
8683
  <router-outlet />
8270
8684
  }
8685
+ <mcms-admin-slot slot="shell:footer" />
8271
8686
  </main>
8272
8687
 
8273
8688
  <mcms-toast-container />
@@ -9090,6 +9505,8 @@ class DashboardPage {
9090
9505
  collectionGroups = computed(() => groupCollections(this.collections()), ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
9091
9506
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DashboardPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
9092
9507
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DashboardPage, isStandalone: true, selector: "mcms-dashboard", host: { classAttribute: "block max-w-6xl" }, ngImport: i0, template: `
9508
+ <mcms-admin-slot slot="dashboard:before" />
9509
+
9093
9510
  <header class="mb-10">
9094
9511
  <h1 class="text-4xl font-bold tracking-tight text-foreground">Dashboard</h1>
9095
9512
  <p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
@@ -9139,16 +9556,20 @@ class DashboardPage {
9139
9556
  </section>
9140
9557
  }
9141
9558
  }
9142
- `, isInline: true, dependencies: [{ kind: "component", type: CollectionCardWidget, selector: "mcms-collection-card", inputs: ["collection", "basePath", "showDocumentCount"], outputs: ["viewAll"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9559
+
9560
+ <mcms-admin-slot slot="dashboard:after" />
9561
+ `, isInline: true, dependencies: [{ kind: "component", type: CollectionCardWidget, selector: "mcms-collection-card", inputs: ["collection", "basePath", "showDocumentCount"], outputs: ["viewAll"] }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9143
9562
  }
9144
9563
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DashboardPage, decorators: [{
9145
9564
  type: Component,
9146
9565
  args: [{
9147
9566
  selector: 'mcms-dashboard',
9148
- imports: [CollectionCardWidget],
9567
+ imports: [CollectionCardWidget, AdminSlotOutlet],
9149
9568
  changeDetection: ChangeDetectionStrategy.OnPush,
9150
9569
  host: { class: 'block max-w-6xl' },
9151
9570
  template: `
9571
+ <mcms-admin-slot slot="dashboard:before" />
9572
+
9152
9573
  <header class="mb-10">
9153
9574
  <h1 class="text-4xl font-bold tracking-tight text-foreground">Dashboard</h1>
9154
9575
  <p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
@@ -9198,6 +9619,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
9198
9619
  </section>
9199
9620
  }
9200
9621
  }
9622
+
9623
+ <mcms-admin-slot slot="dashboard:after" />
9201
9624
  `,
9202
9625
  }]
9203
9626
  }] });
@@ -9344,6 +9767,8 @@ class EntityListWidget {
9344
9767
  route = inject(ActivatedRoute);
9345
9768
  /** Template ref for complex cell rendering (group, array, json). */
9346
9769
  complexCellTemplate = viewChild('complexCell', ...(ngDevMode ? [{ debugName: "complexCellTemplate" }] : []));
9770
+ /** Template ref for badge cell rendering (_status column). */
9771
+ badgeCellTemplate = viewChild('badgeCell', ...(ngDevMode ? [{ debugName: "badgeCellTemplate" }] : []));
9347
9772
  /** The collection configuration */
9348
9773
  collection = input.required(...(ngDevMode ? [{ debugName: "collection" }] : []));
9349
9774
  /** Base path for entity routes */
@@ -9395,8 +9820,17 @@ class EntityListWidget {
9395
9820
  selectedEntities = signal([], ...(ngDevMode ? [{ debugName: "selectedEntities" }] : []));
9396
9821
  searchQuery = model('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
9397
9822
  viewingTrash = signal(false, ...(ngDevMode ? [{ debugName: "viewingTrash" }] : []));
9823
+ statusFilter = signal(null, ...(ngDevMode ? [{ debugName: "statusFilter" }] : []));
9824
+ /** Status filter options for versioned collections */
9825
+ statusFilterOptions = [
9826
+ { label: 'All', value: null },
9827
+ { label: 'Draft', value: 'draft' },
9828
+ { label: 'Published', value: 'published' },
9829
+ ];
9398
9830
  /** Whether the collection has soft delete enabled */
9399
9831
  hasSoftDelete = computed(() => getSoftDeleteField(this.collection()) !== null, ...(ngDevMode ? [{ debugName: "hasSoftDelete" }] : []));
9832
+ /** Whether the collection has versioning with drafts enabled */
9833
+ hasVersioning = computed(() => hasVersionDrafts(this.collection()), ...(ngDevMode ? [{ debugName: "hasVersioning" }] : []));
9400
9834
  /** Computed collection label */
9401
9835
  collectionLabel = computed(() => {
9402
9836
  const col = this.collection();
@@ -9414,8 +9848,9 @@ class EntityListWidget {
9414
9848
  }, ...(ngDevMode ? [{ debugName: "dashboardPath" }] : []));
9415
9849
  /** Auto-derive columns from collection fields if not provided */
9416
9850
  tableColumns = computed(() => {
9417
- // Read template signal at top level so the computed re-runs when viewChild resolves
9851
+ // Read template signals at top level so the computed re-runs when viewChild resolves
9418
9852
  const complexTemplate = this.complexCellTemplate();
9853
+ const badgeTemplate = this.badgeCellTemplate();
9419
9854
  const customColumns = this.columns();
9420
9855
  if (customColumns.length > 0) {
9421
9856
  return customColumns;
@@ -9433,6 +9868,22 @@ class EntityListWidget {
9433
9868
  if (columns.length >= 5)
9434
9869
  break;
9435
9870
  }
9871
+ // Add _status badge column for versioned collections with drafts
9872
+ if (hasVersionDrafts(col)) {
9873
+ columns.push({
9874
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
9875
+ field: '_status',
9876
+ header: 'Status',
9877
+ sortable: true,
9878
+ type: 'badge',
9879
+ width: '110px',
9880
+ badgeMap: {
9881
+ draft: { label: 'Draft', variant: 'secondary' },
9882
+ published: { label: 'Published', variant: 'default' },
9883
+ },
9884
+ ...(badgeTemplate ? { template: badgeTemplate } : {}),
9885
+ });
9886
+ }
9436
9887
  // Add deletedAt column when viewing trash
9437
9888
  if (this.viewingTrash() && this.hasSoftDelete()) {
9438
9889
  const softDeleteField = getSoftDeleteField(col) ?? 'deletedAt';
@@ -9486,6 +9937,11 @@ class EntityListWidget {
9486
9937
  if (typeof initialSearch === 'string') {
9487
9938
  this.searchQuery.set(initialSearch);
9488
9939
  }
9940
+ // Initialize status filter from URL
9941
+ const initialStatus = queryParams['status'];
9942
+ if (initialStatus === 'draft' || initialStatus === 'published') {
9943
+ this.statusFilter.set(initialStatus);
9944
+ }
9489
9945
  // Initialize sort from URL (format: "field" for asc, "-field" for desc)
9490
9946
  const initialSort = queryParams['sort'];
9491
9947
  if (typeof initialSort === 'string' && initialSort) {
@@ -9497,11 +9953,12 @@ class EntityListWidget {
9497
9953
  direction: isDesc ? 'desc' : 'asc',
9498
9954
  });
9499
9955
  }
9500
- // Load data when collection, pagination, or trash view changes
9956
+ // Load data when collection, pagination, trash view, or status filter changes
9501
9957
  effect(() => {
9502
9958
  const col = this.collection();
9503
9959
  const page = this.currentPage();
9504
9960
  const trash = this.viewingTrash();
9961
+ const status = this.statusFilter();
9505
9962
  let sortState = this.sort();
9506
9963
  let search = this.searchQuery();
9507
9964
  if (col) {
@@ -9511,24 +9968,25 @@ class EntityListWidget {
9511
9968
  this.sort.set(undefined);
9512
9969
  this.currentPage.set(1);
9513
9970
  this.viewingTrash.set(false);
9971
+ this.statusFilter.set(null);
9514
9972
  search = '';
9515
9973
  sortState = undefined;
9516
9974
  // Clear URL params
9517
9975
  this.router.navigate([], {
9518
- queryParams: { search: null, sort: null },
9976
+ queryParams: { search: null, sort: null, status: null },
9519
9977
  queryParamsHandling: 'merge',
9520
9978
  replaceUrl: true,
9521
9979
  });
9522
9980
  }
9523
9981
  this.previousCollectionSlug = col.slug;
9524
- this.loadData(col.slug, page, sortState, search, trash);
9982
+ this.loadData(col.slug, page, sortState, search, trash, status);
9525
9983
  }
9526
9984
  });
9527
9985
  }
9528
9986
  /**
9529
9987
  * Load data from API.
9530
9988
  */
9531
- async loadData(slug, page, sortState, search, onlyDeleted) {
9989
+ async loadData(slug, page, sortState, search, onlyDeleted, statusFilter) {
9532
9990
  this.loading.set(true);
9533
9991
  this.error.set(null);
9534
9992
  try {
@@ -9544,17 +10002,25 @@ class EntityListWidget {
9544
10002
  options['sort'] =
9545
10003
  sortState.direction === 'desc' ? `-${String(sortState.field)}` : String(sortState.field);
9546
10004
  }
10005
+ // Build where clause combining search and status filter
10006
+ const whereConditions = [];
9547
10007
  if (search) {
9548
- // Build where clause for search
9549
10008
  const searchFields = this.searchFields().length > 0 ? this.searchFields() : this.getDefaultSearchFields();
9550
10009
  if (searchFields.length > 0) {
9551
- options['where'] = {
9552
- or: searchFields.map((field) => ({
9553
- [field]: { contains: search },
9554
- })),
9555
- };
10010
+ whereConditions.push(...searchFields.map((field) => ({
10011
+ [field]: { contains: search },
10012
+ })));
9556
10013
  }
9557
10014
  }
10015
+ if (statusFilter) {
10016
+ options['where'] = {
10017
+ ...(whereConditions.length > 0 ? { or: whereConditions } : {}),
10018
+ _status: { equals: statusFilter },
10019
+ };
10020
+ }
10021
+ else if (whereConditions.length > 0) {
10022
+ options['where'] = { or: whereConditions };
10023
+ }
9558
10024
  const result = await this.api.collection(slug).find(options);
9559
10025
  this.entities.set(result.docs);
9560
10026
  this.totalItems.set(result.totalDocs);
@@ -9732,6 +10198,15 @@ class EntityListWidget {
9732
10198
  }
9733
10199
  }
9734
10200
  }
10201
+ /**
10202
+ * Look up a badge configuration from a column's badgeMap.
10203
+ */
10204
+ getBadgeConfig(value, column) {
10205
+ if (!column.badgeMap || value === null || value === undefined)
10206
+ return null;
10207
+ const key = String(value);
10208
+ return column.badgeMap[key] ?? null;
10209
+ }
9735
10210
  /**
9736
10211
  * Generate a brief summary string for complex field values (group, array, json).
9737
10212
  */
@@ -9864,7 +10339,7 @@ class EntityListWidget {
9864
10339
  reload() {
9865
10340
  const col = this.collection();
9866
10341
  if (col) {
9867
- this.loadData(col.slug, this.currentPage(), this.sort(), this.searchQuery(), this.viewingTrash());
10342
+ this.loadData(col.slug, this.currentPage(), this.sort(), this.searchQuery(), this.viewingTrash(), this.statusFilter());
9868
10343
  }
9869
10344
  }
9870
10345
  /**
@@ -9875,6 +10350,18 @@ class EntityListWidget {
9875
10350
  this.currentPage.set(1);
9876
10351
  this.selectedEntities.set([]);
9877
10352
  }
10353
+ /**
10354
+ * Set the status filter for versioned collections.
10355
+ */
10356
+ setStatusFilter(status) {
10357
+ this.statusFilter.set(status);
10358
+ this.currentPage.set(1);
10359
+ this.router.navigate([], {
10360
+ queryParams: { status: status ?? null },
10361
+ queryParamsHandling: 'merge',
10362
+ replaceUrl: true,
10363
+ });
10364
+ }
9878
10365
  /**
9879
10366
  * Handle create button click.
9880
10367
  */
@@ -9883,7 +10370,7 @@ class EntityListWidget {
9883
10370
  this.router.navigate([path]);
9884
10371
  }
9885
10372
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EntityListWidget, deps: [], target: i0.ɵɵFactoryTarget.Component });
9886
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: EntityListWidget, isStandalone: true, selector: "mcms-entity-list", inputs: { collection: { classPropertyName: "collection", publicName: "collection", isSignal: true, isRequired: true, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, showBreadcrumbs: { classPropertyName: "showBreadcrumbs", publicName: "showBreadcrumbs", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, bulkActions: { classPropertyName: "bulkActions", publicName: "bulkActions", isSignal: true, isRequired: false, transformFunction: null }, headerActions: { classPropertyName: "headerActions", publicName: "headerActions", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, searchFields: { classPropertyName: "searchFields", publicName: "searchFields", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, paginated: { classPropertyName: "paginated", publicName: "paginated", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, emptyTitle: { classPropertyName: "emptyTitle", publicName: "emptyTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { headerActionClick: "headerActionClick", entityClick: "entityClick", entityAction: "entityAction", bulkAction: "bulkAction", dataLoaded: "dataLoaded", searchQuery: "searchQueryChange" }, host: { classAttribute: "block" }, providers: [provideIcons({ heroEye })], viewQueries: [{ propertyName: "complexCellTemplate", first: true, predicate: ["complexCell"], descendants: true, isSignal: true }], ngImport: i0, template: `
10373
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: EntityListWidget, isStandalone: true, selector: "mcms-entity-list", inputs: { collection: { classPropertyName: "collection", publicName: "collection", isSignal: true, isRequired: true, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, showBreadcrumbs: { classPropertyName: "showBreadcrumbs", publicName: "showBreadcrumbs", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, bulkActions: { classPropertyName: "bulkActions", publicName: "bulkActions", isSignal: true, isRequired: false, transformFunction: null }, headerActions: { classPropertyName: "headerActions", publicName: "headerActions", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, searchFields: { classPropertyName: "searchFields", publicName: "searchFields", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, paginated: { classPropertyName: "paginated", publicName: "paginated", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, emptyTitle: { classPropertyName: "emptyTitle", publicName: "emptyTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { headerActionClick: "headerActionClick", entityClick: "entityClick", entityAction: "entityAction", bulkAction: "bulkAction", dataLoaded: "dataLoaded", searchQuery: "searchQueryChange" }, host: { classAttribute: "block" }, providers: [provideIcons({ heroEye })], viewQueries: [{ propertyName: "complexCellTemplate", first: true, predicate: ["complexCell"], descendants: true, isSignal: true }, { propertyName: "badgeCellTemplate", first: true, predicate: ["badgeCell"], descendants: true, isSignal: true }], ngImport: i0, template: `
9887
10374
  @if (showHeader()) {
9888
10375
  @if (showBreadcrumbs()) {
9889
10376
  <mcms-breadcrumbs class="mb-6">
@@ -9906,6 +10393,22 @@ class EntityListWidget {
9906
10393
  }
9907
10394
  </div>
9908
10395
  <div class="flex items-center gap-2">
10396
+ @if (hasVersioning()) {
10397
+ <div class="flex items-center gap-1" role="group" aria-label="Filter by status">
10398
+ @for (option of statusFilterOptions; track option.value) {
10399
+ <button
10400
+ mcms-button
10401
+ [variant]="statusFilter() === option.value ? 'outline' : 'ghost'"
10402
+ size="sm"
10403
+ [attr.data-testid]="'status-filter-' + (option.value ?? 'all')"
10404
+ [attr.aria-pressed]="statusFilter() === option.value"
10405
+ (click)="setStatusFilter(option.value)"
10406
+ >
10407
+ {{ option.label }}
10408
+ </button>
10409
+ }
10410
+ </div>
10411
+ }
9909
10412
  @if (hasSoftDelete()) {
9910
10413
  <button
9911
10414
  mcms-button
@@ -9979,6 +10482,15 @@ class EntityListWidget {
9979
10482
  }
9980
10483
  </mcms-data-table>
9981
10484
 
10485
+ <!-- Template for badge cells (_status column) -->
10486
+ <ng-template #badgeCell let-value let-column="column">
10487
+ @if (getBadgeConfig(value, column); as badge) {
10488
+ <mcms-badge [variant]="badge.variant">{{ badge.label }}</mcms-badge>
10489
+ } @else {
10490
+ {{ value ?? '-' }}
10491
+ }
10492
+ </ng-template>
10493
+
9982
10494
  <!-- Template for complex field cells (group, array, json) -->
9983
10495
  <ng-template #complexCell let-value let-column="column">
9984
10496
  <div class="flex items-center gap-1.5">
@@ -10030,6 +10542,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
10030
10542
  }
10031
10543
  </div>
10032
10544
  <div class="flex items-center gap-2">
10545
+ @if (hasVersioning()) {
10546
+ <div class="flex items-center gap-1" role="group" aria-label="Filter by status">
10547
+ @for (option of statusFilterOptions; track option.value) {
10548
+ <button
10549
+ mcms-button
10550
+ [variant]="statusFilter() === option.value ? 'outline' : 'ghost'"
10551
+ size="sm"
10552
+ [attr.data-testid]="'status-filter-' + (option.value ?? 'all')"
10553
+ [attr.aria-pressed]="statusFilter() === option.value"
10554
+ (click)="setStatusFilter(option.value)"
10555
+ >
10556
+ {{ option.label }}
10557
+ </button>
10558
+ }
10559
+ </div>
10560
+ }
10033
10561
  @if (hasSoftDelete()) {
10034
10562
  <button
10035
10563
  mcms-button
@@ -10103,6 +10631,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
10103
10631
  }
10104
10632
  </mcms-data-table>
10105
10633
 
10634
+ <!-- Template for badge cells (_status column) -->
10635
+ <ng-template #badgeCell let-value let-column="column">
10636
+ @if (getBadgeConfig(value, column); as badge) {
10637
+ <mcms-badge [variant]="badge.variant">{{ badge.label }}</mcms-badge>
10638
+ } @else {
10639
+ {{ value ?? '-' }}
10640
+ }
10641
+ </ng-template>
10642
+
10106
10643
  <!-- Template for complex field cells (group, array, json) -->
10107
10644
  <ng-template #complexCell let-value let-column="column">
10108
10645
  <div class="flex items-center gap-1.5">
@@ -10123,7 +10660,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
10123
10660
  </ng-template>
10124
10661
  `,
10125
10662
  }]
10126
- }], ctorParameters: () => [], propDecorators: { complexCellTemplate: [{ type: i0.ViewChild, args: ['complexCell', { isSignal: true }] }], collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], basePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "basePath", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], showBreadcrumbs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBreadcrumbs", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], bulkActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "bulkActions", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], headerActionClick: [{ type: i0.Output, args: ["headerActionClick"] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], searchFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFields", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], paginated: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginated", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], emptyTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTitle", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], entityClick: [{ type: i0.Output, args: ["entityClick"] }], entityAction: [{ type: i0.Output, args: ["entityAction"] }], bulkAction: [{ type: i0.Output, args: ["bulkAction"] }], dataLoaded: [{ type: i0.Output, args: ["dataLoaded"] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }] } });
10663
+ }], ctorParameters: () => [], propDecorators: { complexCellTemplate: [{ type: i0.ViewChild, args: ['complexCell', { isSignal: true }] }], badgeCellTemplate: [{ type: i0.ViewChild, args: ['badgeCell', { isSignal: true }] }], collection: [{ type: i0.Input, args: [{ isSignal: true, alias: "collection", required: true }] }], basePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "basePath", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], showBreadcrumbs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBreadcrumbs", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], bulkActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "bulkActions", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], headerActionClick: [{ type: i0.Output, args: ["headerActionClick"] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], searchFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFields", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], paginated: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginated", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], emptyTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTitle", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], entityClick: [{ type: i0.Output, args: ["entityClick"] }], entityAction: [{ type: i0.Output, args: ["entityAction"] }], bulkAction: [{ type: i0.Output, args: ["bulkAction"] }], dataLoaded: [{ type: i0.Output, args: ["dataLoaded"] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }] } });
10127
10664
 
10128
10665
  /** Role hierarchy: lower index = higher privilege.
10129
10666
  * Keep in sync with AUTH_ROLES in @momentumcms/auth/collections */
@@ -10510,6 +11047,7 @@ class CollectionListPage {
10510
11047
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionListPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
10511
11048
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CollectionListPage, isStandalone: true, selector: "mcms-collection-list", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "entityList", first: true, predicate: ["entityList"], descendants: true, isSignal: true }], ngImport: i0, template: `
10512
11049
  @if (collection(); as col) {
11050
+ <mcms-admin-slot slot="collection-list:before" [collectionSlug]="col.slug" />
10513
11051
  <mcms-entity-list
10514
11052
  #entityList
10515
11053
  [collection]="col"
@@ -10521,20 +11059,22 @@ class CollectionListPage {
10521
11059
  (bulkAction)="onBulkAction($event)"
10522
11060
  (headerActionClick)="onHeaderAction($event)"
10523
11061
  />
11062
+ <mcms-admin-slot slot="collection-list:after" [collectionSlug]="col.slug" />
10524
11063
  } @else {
10525
11064
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
10526
11065
  }
10527
- `, isInline: true, dependencies: [{ kind: "component", type: EntityListWidget, selector: "mcms-entity-list", inputs: ["collection", "basePath", "showHeader", "showBreadcrumbs", "columns", "rowActions", "bulkActions", "headerActions", "searchable", "searchPlaceholder", "searchFields", "sortable", "selectable", "paginated", "pageSize", "emptyTitle", "emptyDescription", "searchQuery"], outputs: ["headerActionClick", "entityClick", "entityAction", "bulkAction", "dataLoaded", "searchQueryChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11066
+ `, isInline: true, dependencies: [{ kind: "component", type: EntityListWidget, selector: "mcms-entity-list", inputs: ["collection", "basePath", "showHeader", "showBreadcrumbs", "columns", "rowActions", "bulkActions", "headerActions", "searchable", "searchPlaceholder", "searchFields", "sortable", "selectable", "paginated", "pageSize", "emptyTitle", "emptyDescription", "searchQuery"], outputs: ["headerActionClick", "entityClick", "entityAction", "bulkAction", "dataLoaded", "searchQueryChange"] }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10528
11067
  }
10529
11068
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionListPage, decorators: [{
10530
11069
  type: Component,
10531
11070
  args: [{
10532
11071
  selector: 'mcms-collection-list',
10533
- imports: [EntityListWidget],
11072
+ imports: [EntityListWidget, AdminSlotOutlet],
10534
11073
  changeDetection: ChangeDetectionStrategy.OnPush,
10535
11074
  host: { class: 'block' },
10536
11075
  template: `
10537
11076
  @if (collection(); as col) {
11077
+ <mcms-admin-slot slot="collection-list:before" [collectionSlug]="col.slug" />
10538
11078
  <mcms-entity-list
10539
11079
  #entityList
10540
11080
  [collection]="col"
@@ -10546,6 +11086,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
10546
11086
  (bulkAction)="onBulkAction($event)"
10547
11087
  (headerActionClick)="onHeaderAction($event)"
10548
11088
  />
11089
+ <mcms-admin-slot slot="collection-list:after" [collectionSlug]="col.slug" />
10549
11090
  } @else {
10550
11091
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
10551
11092
  }
@@ -10595,6 +11136,7 @@ class CollectionViewPage {
10595
11136
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionViewPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
10596
11137
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CollectionViewPage, isStandalone: true, selector: "mcms-collection-view", host: { classAttribute: "block" }, ngImport: i0, template: `
10597
11138
  @if (collection(); as col) {
11139
+ <mcms-admin-slot slot="collection-view:before" [collectionSlug]="col.slug" />
10598
11140
  @if (entityId(); as id) {
10599
11141
  <mcms-entity-view
10600
11142
  [collection]="col"
@@ -10606,20 +11148,22 @@ class CollectionViewPage {
10606
11148
  } @else {
10607
11149
  <div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
10608
11150
  }
11151
+ <mcms-admin-slot slot="collection-view:after" [collectionSlug]="col.slug" />
10609
11152
  } @else {
10610
11153
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
10611
11154
  }
10612
- `, isInline: true, dependencies: [{ kind: "component", type: EntityViewWidget, selector: "mcms-entity-view", inputs: ["collection", "entityId", "basePath", "showBreadcrumbs", "fieldConfigs", "actions", "showVersionHistory", "suppressNavigation"], outputs: ["edit", "statusChanged", "delete_", "actionClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11155
+ `, isInline: true, dependencies: [{ kind: "component", type: EntityViewWidget, selector: "mcms-entity-view", inputs: ["collection", "entityId", "basePath", "showBreadcrumbs", "fieldConfigs", "actions", "showVersionHistory", "suppressNavigation"], outputs: ["edit", "statusChanged", "delete_", "actionClick"] }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10613
11156
  }
10614
11157
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionViewPage, decorators: [{
10615
11158
  type: Component,
10616
11159
  args: [{
10617
11160
  selector: 'mcms-collection-view',
10618
- imports: [EntityViewWidget],
11161
+ imports: [EntityViewWidget, AdminSlotOutlet],
10619
11162
  changeDetection: ChangeDetectionStrategy.OnPush,
10620
11163
  host: { class: 'block' },
10621
11164
  template: `
10622
11165
  @if (collection(); as col) {
11166
+ <mcms-admin-slot slot="collection-view:before" [collectionSlug]="col.slug" />
10623
11167
  @if (entityId(); as id) {
10624
11168
  <mcms-entity-view
10625
11169
  [collection]="col"
@@ -10631,6 +11175,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
10631
11175
  } @else {
10632
11176
  <div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
10633
11177
  }
11178
+ <mcms-admin-slot slot="collection-view:after" [collectionSlug]="col.slug" />
10634
11179
  } @else {
10635
11180
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
10636
11181
  }
@@ -11245,6 +11790,7 @@ class CollectionEditPage {
11245
11790
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionEditPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
11246
11791
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CollectionEditPage, isStandalone: true, selector: "mcms-collection-edit", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "entityFormRef", first: true, predicate: ["entityForm"], descendants: true, isSignal: true }], ngImport: i0, template: `
11247
11792
  @if (collection(); as col) {
11793
+ <mcms-admin-slot slot="collection-edit:before" [collectionSlug]="col.slug" />
11248
11794
  @if (previewConfig(); as preview) {
11249
11795
  @if (showPreview()) {
11250
11796
  <!-- Split layout: form + preview -->
@@ -11279,6 +11825,7 @@ class CollectionEditPage {
11279
11825
  (editBlockRequest)="onEditBlockRequest($event)"
11280
11826
  />
11281
11827
  </div>
11828
+ <mcms-admin-slot slot="collection-edit:sidebar" [collectionSlug]="col.slug" />
11282
11829
  </div>
11283
11830
  } @else {
11284
11831
  <!-- Full-width form (preview hidden) -->
@@ -11303,29 +11850,36 @@ class CollectionEditPage {
11303
11850
  </mcms-entity-form>
11304
11851
  }
11305
11852
  } @else {
11306
- <!-- Standard layout: form only -->
11307
- <mcms-entity-form
11308
- #entityForm
11309
- [collection]="col"
11310
- [entityId]="entityId()"
11311
- [mode]="mode()"
11312
- [basePath]="basePath"
11313
- />
11853
+ <!-- Standard layout: form + optional sidebar -->
11854
+ <div class="flex gap-6">
11855
+ <div class="min-w-0 flex-1">
11856
+ <mcms-entity-form
11857
+ #entityForm
11858
+ [collection]="col"
11859
+ [entityId]="entityId()"
11860
+ [mode]="mode()"
11861
+ [basePath]="basePath"
11862
+ />
11863
+ </div>
11864
+ <mcms-admin-slot slot="collection-edit:sidebar" [collectionSlug]="col.slug" />
11865
+ </div>
11314
11866
  }
11867
+ <mcms-admin-slot slot="collection-edit:after" [collectionSlug]="col.slug" />
11315
11868
  } @else {
11316
11869
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
11317
11870
  }
11318
- `, isInline: true, dependencies: [{ kind: "component", type: EntityFormWidget, selector: "mcms-entity-form", inputs: ["collection", "entityId", "mode", "basePath", "showBreadcrumbs", "suppressNavigation", "isGlobal", "globalSlug"], outputs: ["saved", "cancelled", "saveError", "modeChange", "draftSaved"] }, { kind: "component", type: LivePreviewComponent, selector: "mcms-live-preview", inputs: ["preview", "documentData", "collectionSlug", "entityId"], outputs: ["editBlockRequest"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11871
+ `, isInline: true, dependencies: [{ kind: "component", type: EntityFormWidget, selector: "mcms-entity-form", inputs: ["collection", "entityId", "mode", "basePath", "showBreadcrumbs", "suppressNavigation", "isGlobal", "globalSlug"], outputs: ["saved", "cancelled", "saveError", "modeChange", "draftSaved"] }, { kind: "component", type: LivePreviewComponent, selector: "mcms-live-preview", inputs: ["preview", "documentData", "collectionSlug", "entityId"], outputs: ["editBlockRequest"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11319
11872
  }
11320
11873
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CollectionEditPage, decorators: [{
11321
11874
  type: Component,
11322
11875
  args: [{
11323
11876
  selector: 'mcms-collection-edit',
11324
- imports: [EntityFormWidget, LivePreviewComponent, Button],
11877
+ imports: [EntityFormWidget, LivePreviewComponent, Button, AdminSlotOutlet],
11325
11878
  changeDetection: ChangeDetectionStrategy.OnPush,
11326
11879
  host: { class: 'block' },
11327
11880
  template: `
11328
11881
  @if (collection(); as col) {
11882
+ <mcms-admin-slot slot="collection-edit:before" [collectionSlug]="col.slug" />
11329
11883
  @if (previewConfig(); as preview) {
11330
11884
  @if (showPreview()) {
11331
11885
  <!-- Split layout: form + preview -->
@@ -11360,6 +11914,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
11360
11914
  (editBlockRequest)="onEditBlockRequest($event)"
11361
11915
  />
11362
11916
  </div>
11917
+ <mcms-admin-slot slot="collection-edit:sidebar" [collectionSlug]="col.slug" />
11363
11918
  </div>
11364
11919
  } @else {
11365
11920
  <!-- Full-width form (preview hidden) -->
@@ -11384,15 +11939,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
11384
11939
  </mcms-entity-form>
11385
11940
  }
11386
11941
  } @else {
11387
- <!-- Standard layout: form only -->
11388
- <mcms-entity-form
11389
- #entityForm
11390
- [collection]="col"
11391
- [entityId]="entityId()"
11392
- [mode]="mode()"
11393
- [basePath]="basePath"
11394
- />
11942
+ <!-- Standard layout: form + optional sidebar -->
11943
+ <div class="flex gap-6">
11944
+ <div class="min-w-0 flex-1">
11945
+ <mcms-entity-form
11946
+ #entityForm
11947
+ [collection]="col"
11948
+ [entityId]="entityId()"
11949
+ [mode]="mode()"
11950
+ [basePath]="basePath"
11951
+ />
11952
+ </div>
11953
+ <mcms-admin-slot slot="collection-edit:sidebar" [collectionSlug]="col.slug" />
11954
+ </div>
11395
11955
  }
11956
+ <mcms-admin-slot slot="collection-edit:after" [collectionSlug]="col.slug" />
11396
11957
  } @else {
11397
11958
  <div class="p-12 text-center text-muted-foreground">Collection not found</div>
11398
11959
  }
@@ -11485,6 +12046,7 @@ class LoginPage {
11485
12046
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LoginPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
11486
12047
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: LoginPage, isStandalone: true, selector: "mcms-login-page", host: { classAttribute: "flex min-h-screen items-center justify-center bg-background p-4" }, ngImport: i0, template: `
11487
12048
  <main>
12049
+ <mcms-admin-slot slot="login:before" />
11488
12050
  <mcms-card class="w-full max-w-md">
11489
12051
  <mcms-card-header class="text-center">
11490
12052
  <mcms-card-title>Sign In</mcms-card-title>
@@ -11622,8 +12184,9 @@ class LoginPage {
11622
12184
  <p class="text-sm text-muted-foreground">Momentum CMS</p>
11623
12185
  </mcms-card-footer>
11624
12186
  </mcms-card>
12187
+ <mcms-admin-slot slot="login:after" />
11625
12188
  </main>
11626
- `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: Input, selector: "mcms-input", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "type", "id", "name", "placeholder", "autocomplete", "ariaLabel", "describedBy", "min", "max", "step"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: McmsFormField, selector: "mcms-form-field", inputs: ["id", "required", "disabled", "errors", "hint", "hasLabel"] }, { kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardHeader, selector: "mcms-card-header" }, { kind: "component", type: CardTitle, selector: "mcms-card-title", inputs: ["level"] }, { kind: "component", type: CardDescription, selector: "mcms-card-description" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12189
+ `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: Input, selector: "mcms-input", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "type", "id", "name", "placeholder", "autocomplete", "ariaLabel", "describedBy", "min", "max", "step"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: McmsFormField, selector: "mcms-form-field", inputs: ["id", "required", "disabled", "errors", "hint", "hasLabel"] }, { kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardHeader, selector: "mcms-card-header" }, { kind: "component", type: CardTitle, selector: "mcms-card-title", inputs: ["level"] }, { kind: "component", type: CardDescription, selector: "mcms-card-description" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: AdminSlotOutlet, selector: "mcms-admin-slot", inputs: ["slot", "collectionSlug", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11627
12190
  }
11628
12191
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LoginPage, decorators: [{
11629
12192
  type: Component,
@@ -11640,6 +12203,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
11640
12203
  CardDescription,
11641
12204
  CardContent,
11642
12205
  CardFooter,
12206
+ AdminSlotOutlet,
11643
12207
  ],
11644
12208
  changeDetection: ChangeDetectionStrategy.OnPush,
11645
12209
  host: {
@@ -11647,6 +12211,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
11647
12211
  },
11648
12212
  template: `
11649
12213
  <main>
12214
+ <mcms-admin-slot slot="login:before" />
11650
12215
  <mcms-card class="w-full max-w-md">
11651
12216
  <mcms-card-header class="text-center">
11652
12217
  <mcms-card-title>Sign In</mcms-card-title>
@@ -11784,6 +12349,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
11784
12349
  <p class="text-sm text-muted-foreground">Momentum CMS</p>
11785
12350
  </mcms-card-footer>
11786
12351
  </mcms-card>
12352
+ <mcms-admin-slot slot="login:after" />
11787
12353
  </main>
11788
12354
  `,
11789
12355
  }]
@@ -14222,18 +14788,18 @@ function provideMomentumFieldRenderers() {
14222
14788
  registry.register('checkbox', () => Promise.resolve().then(function () { return checkboxField_component; }).then((m) => m.CheckboxFieldRenderer));
14223
14789
  registry.register('date', () => Promise.resolve().then(function () { return dateField_component; }).then((m) => m.DateFieldRenderer));
14224
14790
  registry.register('upload', () => Promise.resolve().then(function () { return uploadField_component; }).then((m) => m.UploadFieldRenderer));
14225
- registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs').then((m) => m.RichTextFieldRenderer));
14791
+ registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-CZQSu4lc.mjs').then((m) => m.RichTextFieldRenderer));
14226
14792
  // Layout field renderers (support nested field rendering)
14227
- registry.register('group', () => import('./momentumcms-admin-group-field.component-Bofhumd5.mjs').then((m) => m.GroupFieldRenderer));
14228
- registry.register('array', () => import('./momentumcms-admin-array-field.component-ChjP5zK9.mjs').then((m) => m.ArrayFieldRenderer));
14229
- registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs').then((m) => m.BlocksFieldRenderer));
14793
+ registry.register('group', () => import('./momentumcms-admin-group-field.component-qy0Z-cRK.mjs').then((m) => m.GroupFieldRenderer));
14794
+ registry.register('array', () => import('./momentumcms-admin-array-field.component-BLL21bqy.mjs').then((m) => m.ArrayFieldRenderer));
14795
+ registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-BQHIguqN.mjs').then((m) => m.BlocksFieldRenderer));
14230
14796
  // Visual block editor variant (blocks field with admin.editor === 'visual')
14231
14797
  registry.register('blocks-visual', () => Promise.resolve().then(function () { return visualBlockEditor_component; }).then((m) => m.VisualBlockEditorComponent));
14232
- registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-DSZhc5MP.mjs').then((m) => m.RelationshipFieldRenderer));
14798
+ registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-DS68G61P.mjs').then((m) => m.RelationshipFieldRenderer));
14233
14799
  // Layout-only renderers (tabs, collapsible, row)
14234
- registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-BsnCWC5J.mjs').then((m) => m.TabsFieldRenderer));
14235
- registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs').then((m) => m.CollapsibleFieldRenderer));
14236
- registry.register('row', () => import('./momentumcms-admin-row-field.component-DBzqzooT.mjs').then((m) => m.RowFieldRenderer));
14800
+ registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-DG8vPjj4.mjs').then((m) => m.TabsFieldRenderer));
14801
+ registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-DSE4X1xV.mjs').then((m) => m.CollapsibleFieldRenderer));
14802
+ registry.register('row', () => import('./momentumcms-admin-row-field.component-109gO-Rp.mjs').then((m) => m.RowFieldRenderer));
14237
14803
  };
14238
14804
  },
14239
14805
  },
@@ -14268,6 +14834,104 @@ function provideFieldRenderer(type, loader) {
14268
14834
  ]);
14269
14835
  }
14270
14836
 
14837
+ /**
14838
+ * Registry-aware page resolver that delegates to either a registered
14839
+ * custom component or the built-in fallback.
14840
+ *
14841
+ * Used as the `loadComponent` for every admin route. Route `data` provides:
14842
+ * - `adminPageKey` — the registry key (e.g., 'dashboard', 'collection-list')
14843
+ * - `adminPageFallback` — lazy loader for the built-in default
14844
+ *
14845
+ * For collection pages, reads `:slug` from route params to check per-collection overrides.
14846
+ *
14847
+ * Subscribes to route observables so the component re-resolves when Angular
14848
+ * reuses the instance for sibling routes (e.g., navigating between collections).
14849
+ *
14850
+ * Implements HasUnsavedChanges to delegate to the resolved component for
14851
+ * the unsaved changes guard.
14852
+ */
14853
+ class AdminPageResolver {
14854
+ registry = inject(AdminComponentRegistry);
14855
+ route = inject(ActivatedRoute);
14856
+ destroyRef = inject(DestroyRef);
14857
+ outlet = viewChild(NgComponentOutlet, ...(ngDevMode ? [{ debugName: "outlet" }] : []));
14858
+ resolvedComponent = signal(null, ...(ngDevMode ? [{ debugName: "resolvedComponent" }] : []));
14859
+ /** Incremented on every load to detect stale promise resolutions. */
14860
+ loadGeneration = 0;
14861
+ ngOnInit() {
14862
+ combineLatest([this.route.data, this.route.params])
14863
+ .pipe(takeUntilDestroyed(this.destroyRef))
14864
+ .subscribe(([data, params]) => {
14865
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- route data is Record<string, unknown>
14866
+ const pageKey = data['adminPageKey'];
14867
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- route data is Record<string, unknown>
14868
+ const fallback = data['adminPageFallback'];
14869
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- route params are Record<string, string>
14870
+ const slug = params['slug'];
14871
+ const override = this.registry.resolve(pageKey, slug);
14872
+ const loader = override ?? fallback;
14873
+ if (loader) {
14874
+ const generation = ++this.loadGeneration;
14875
+ loader()
14876
+ .then((component) => {
14877
+ if (generation !== this.loadGeneration)
14878
+ return;
14879
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- loader resolves to unknown from registry, safe cast to Type
14880
+ this.resolvedComponent.set(component);
14881
+ })
14882
+ .catch((err) => {
14883
+ if (generation !== this.loadGeneration)
14884
+ return;
14885
+ console.error('[AdminPageResolver] Failed to load component:', err);
14886
+ });
14887
+ }
14888
+ });
14889
+ }
14890
+ hasUnsavedChanges() {
14891
+ const instance = this.outlet()?.componentInstance;
14892
+ if (instance == null)
14893
+ return false;
14894
+ // Check if the resolved component implements HasUnsavedChanges
14895
+ if ('hasUnsavedChanges' in instance && typeof instance.hasUnsavedChanges === 'function') {
14896
+ return Boolean(instance.hasUnsavedChanges());
14897
+ }
14898
+ return false;
14899
+ }
14900
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminPageResolver, deps: [], target: i0.ɵɵFactoryTarget.Component });
14901
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AdminPageResolver, isStandalone: true, selector: "mcms-admin-page-resolver", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "outlet", first: true, predicate: NgComponentOutlet, descendants: true, isSignal: true }], ngImport: i0, template: `
14902
+ @if (resolvedComponent()) {
14903
+ <ng-container *ngComponentOutlet="resolvedComponent()" />
14904
+ } @else {
14905
+ <div role="status" aria-label="Loading page" class="flex h-full items-center justify-center">
14906
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary"></div>
14907
+ </div>
14908
+ }
14909
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14910
+ }
14911
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminPageResolver, decorators: [{
14912
+ type: Component,
14913
+ args: [{
14914
+ selector: 'mcms-admin-page-resolver',
14915
+ imports: [NgComponentOutlet],
14916
+ changeDetection: ChangeDetectionStrategy.OnPush,
14917
+ host: { class: 'block' },
14918
+ template: `
14919
+ @if (resolvedComponent()) {
14920
+ <ng-container *ngComponentOutlet="resolvedComponent()" />
14921
+ } @else {
14922
+ <div role="status" aria-label="Loading page" class="flex h-full items-center justify-center">
14923
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary"></div>
14924
+ </div>
14925
+ }
14926
+ `,
14927
+ }]
14928
+ }], propDecorators: { outlet: [{ type: i0.ViewChild, args: [i0.forwardRef(() => NgComponentOutlet), { isSignal: true }] }] } });
14929
+
14930
+ var adminPageResolver_component = /*#__PURE__*/Object.freeze({
14931
+ __proto__: null,
14932
+ AdminPageResolver: AdminPageResolver
14933
+ });
14934
+
14271
14935
  /* eslint-disable @typescript-eslint/consistent-type-assertions -- Type assertions needed to narrow Field union to TextField/TextareaField after type guard */
14272
14936
  /**
14273
14937
  * Text field renderer for text and textarea field types.
@@ -16012,5 +16676,5 @@ var uploadField_component = /*#__PURE__*/Object.freeze({
16012
16676
  * Generated bundle index. Do not edit.
16013
16677
  */
16014
16678
 
16015
- export { adminGuard as $, AdminShellComponent as A, BlockEditDialog as B, CheckboxFieldRenderer as C, DashboardPage as D, EntityFormWidget as E, FieldRenderer as F, MediaLibraryPage as G, MediaPickerDialog as H, MediaPreviewComponent as I, MomentumApiService as J, MomentumAuthService as K, LivePreviewComponent as L, MOMENTUM_API as M, NumberFieldRenderer as N, ResetPasswordPage as O, PublishControlsWidget as P, SKIP_AUTO_TOAST as Q, ResetPasswordFormComponent as R, SHEET_QUERY_PARAMS as S, SelectFieldRenderer as T, SetupPage as U, TextFieldRenderer as V, UploadFieldRenderer as W, UploadService as X, VersionHistoryWidget as Y, VersionService as Z, VisualBlockEditorComponent as _, getFieldNodeState as a, authGuard as a0, collectionAccessGuard as a1, crudToastInterceptor as a2, guestGuard as a3, injectHasAnyRole as a4, injectHasRole as a5, injectIsAdmin as a6, injectIsAuthenticated as a7, injectMomentumAPI as a8, injectTypedMomentumAPI as a9, injectUser as aa, injectUserRole as ab, injectVersionService as ac, momentumAdminRoutes as ad, provideFieldRenderer as ae, provideMomentumAPI as af, provideMomentumFieldRenderers as ag, setupGuard as ah, unsavedChangesGuard as ai, getSubNode as b, getFieldDefaultValue as c, EntitySheetService as d, getTitleField as e, AdminSidebarWidget as f, getGlobalsFromRouteData as g, BlockInserterComponent as h, isRecord as i, BlockWrapperComponent as j, CollectionAccessService as k, CollectionCardWidget as l, CollectionEditPage as m, normalizeBlockDefaults as n, CollectionListPage as o, CollectionViewPage as p, DateFieldRenderer as q, EntityListWidget as r, EntityViewWidget as s, FeedbackService as t, FieldRendererRegistry as u, ForgotPasswordFormComponent as v, ForgotPasswordPage as w, LoginPage as x, MOMENTUM_API_CONTEXT as y, McmsThemeService as z };
16016
- //# sourceMappingURL=momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs.map
16679
+ export { UploadService as $, AdminComponentRegistry as A, BlockEditDialog as B, CheckboxFieldRenderer as C, DashboardPage as D, EntityFormWidget as E, FieldRenderer as F, ForgotPasswordPage as G, LoginPage as H, MOMENTUM_API_CONTEXT as I, McmsThemeService as J, MediaLibraryPage as K, LivePreviewComponent as L, MOMENTUM_API as M, MediaPickerDialog as N, MediaPreviewComponent as O, MomentumApiService as P, MomentumAuthService as Q, NumberFieldRenderer as R, PublishControlsWidget as S, ResetPasswordFormComponent as T, ResetPasswordPage as U, SHEET_QUERY_PARAMS as V, SKIP_AUTO_TOAST as W, SelectFieldRenderer as X, SetupPage as Y, TextFieldRenderer as Z, UploadFieldRenderer as _, getFieldNodeState as a, VersionHistoryWidget as a0, VersionService as a1, VisualBlockEditorComponent as a2, adminGuard as a3, authGuard as a4, collectionAccessGuard as a5, crudToastInterceptor as a6, guestGuard as a7, injectHasAnyRole as a8, injectHasRole as a9, injectIsAdmin as aa, injectIsAuthenticated as ab, injectMomentumAPI as ac, injectTypedMomentumAPI as ad, injectUser as ae, injectUserRole as af, injectVersionService as ag, momentumAdminRoutes as ah, provideAdminComponent as ai, provideAdminSlot as aj, provideFieldRenderer as ak, provideMomentumAPI as al, provideMomentumFieldRenderers as am, registerConfigComponents as an, setupGuard as ao, unsavedChangesGuard as ap, getSubNode as b, getFieldDefaultValue as c, EntitySheetService as d, getTitleField as e, AdminPageResolver as f, getGlobalsFromRouteData as g, AdminShellComponent as h, isRecord as i, AdminSidebarWidget as j, AdminSlotOutlet as k, AdminSlotRegistry as l, BlockInserterComponent as m, normalizeBlockDefaults as n, BlockWrapperComponent as o, CollectionAccessService as p, CollectionCardWidget as q, CollectionEditPage as r, CollectionListPage as s, CollectionViewPage as t, DateFieldRenderer as u, EntityListWidget as v, EntityViewWidget as w, FeedbackService as x, FieldRendererRegistry as y, ForgotPasswordFormComponent as z };
16680
+ //# sourceMappingURL=momentumcms-admin-momentumcms-admin-DDwm1Rm3.mjs.map