@momentumcms/admin 0.1.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{momentumcms-admin-array-field.component-BZva87Sh.mjs → momentumcms-admin-array-field.component-Bjlcczwg.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-array-field.component-BZva87Sh.mjs.map → momentumcms-admin-array-field.component-Bjlcczwg.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-blocks-field.component-CIxpyKAV.mjs → momentumcms-admin-blocks-field.component-4vLqDGbB.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-blocks-field.component-CIxpyKAV.mjs.map → momentumcms-admin-blocks-field.component-4vLqDGbB.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-collapsible-field.component-BpmaUKom.mjs → momentumcms-admin-collapsible-field.component-63-9kSgm.mjs} +3 -9
- package/fesm2022/momentumcms-admin-collapsible-field.component-63-9kSgm.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-global-edit.page-976NlNjw.mjs → momentumcms-admin-global-edit.page-DSnkwdgn.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-global-edit.page-976NlNjw.mjs.map → momentumcms-admin-global-edit.page-DSnkwdgn.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-group-field.component-Bgy_tQOG.mjs → momentumcms-admin-group-field.component-B48_zbo0.mjs} +2 -2
- package/fesm2022/momentumcms-admin-group-field.component-B48_zbo0.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-momentumcms-admin-TvEIOeYg.mjs → momentumcms-admin-momentumcms-admin-D_47TVaR.mjs} +1543 -751
- package/fesm2022/momentumcms-admin-momentumcms-admin-D_47TVaR.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-relationship-field.component-s46Lu33u.mjs → momentumcms-admin-relationship-field.component-D-UQgd7m.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-relationship-field.component-s46Lu33u.mjs.map → momentumcms-admin-relationship-field.component-D-UQgd7m.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-rich-text-field.component-Djv7tDS2.mjs → momentumcms-admin-rich-text-field.component-BC8pRU89.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-rich-text-field.component-Djv7tDS2.mjs.map → momentumcms-admin-rich-text-field.component-BC8pRU89.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-row-field.component-Dc5vqRQ8.mjs → momentumcms-admin-row-field.component--EOPGDtM.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-row-field.component-Dc5vqRQ8.mjs.map → momentumcms-admin-row-field.component--EOPGDtM.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-tabs-field.component-CdZoCrvw.mjs → momentumcms-admin-tabs-field.component-B4X73eCM.mjs} +73 -11
- package/fesm2022/momentumcms-admin-tabs-field.component-B4X73eCM.mjs.map +1 -0
- package/fesm2022/momentumcms-admin.mjs +1 -1
- package/package.json +1 -1
- package/types/momentumcms-admin.d.ts +92 -29
- package/fesm2022/momentumcms-admin-collapsible-field.component-BpmaUKom.mjs.map +0 -1
- package/fesm2022/momentumcms-admin-group-field.component-Bgy_tQOG.mjs.map +0 -1
- package/fesm2022/momentumcms-admin-momentumcms-admin-TvEIOeYg.mjs.map +0 -1
- package/fesm2022/momentumcms-admin-tabs-field.component-CdZoCrvw.mjs.map +0 -1
|
@@ -1,19 +1,18 @@
|
|
|
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, Injector, untracked, runInInjectionContext, afterNextRender,
|
|
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';
|
|
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
6
|
import { firstValueFrom, tap, catchError, throwError, Subject, finalize, Observable, of, filter, take } from 'rxjs';
|
|
7
|
-
import { ToastService, ConfirmationService, DIALOG_DATA, Dialog, DialogHeader, DialogTitle, DialogContent, DialogFooter, DialogClose, Button, Badge, Skeleton, DialogService, Card, CardHeader, CardContent, Separator, 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
|
|
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, getSoftDeleteField } from '@momentumcms/core';
|
|
12
|
+
import { flattenDataFields, humanizeFieldName, isUploadCollection, getSoftDeleteField } from '@momentumcms/core';
|
|
13
13
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
14
|
-
import {
|
|
14
|
+
import { heroFilm, heroMusicalNote, heroDocumentText, heroArchiveBox, heroPhoto, heroDocument, heroCloudArrowUp, heroXMark, 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';
|
|
16
|
-
import { DomSanitizer } from '@angular/platform-browser';
|
|
17
16
|
import { moveItemInArray, CdkDropList, CdkDrag, CdkDragPlaceholder } from '@angular/cdk/drag-drop';
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -1135,7 +1134,7 @@ function momentumAdminRoutes(configOrOptions) {
|
|
|
1135
1134
|
// Global edit
|
|
1136
1135
|
{
|
|
1137
1136
|
path: 'globals/:slug',
|
|
1138
|
-
loadComponent: () => import('./momentumcms-admin-global-edit.page-
|
|
1137
|
+
loadComponent: () => import('./momentumcms-admin-global-edit.page-DSnkwdgn.mjs').then((m) => m.GlobalEditPage),
|
|
1139
1138
|
canDeactivate: [unsavedChangesGuard],
|
|
1140
1139
|
},
|
|
1141
1140
|
// Plugin-registered routes
|
|
@@ -1539,6 +1538,95 @@ class UploadService {
|
|
|
1539
1538
|
});
|
|
1540
1539
|
return subject.asObservable();
|
|
1541
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Upload a file to a specific upload collection.
|
|
1543
|
+
* POSTs multipart/form-data to /api/{collectionSlug} with file + additional fields.
|
|
1544
|
+
*
|
|
1545
|
+
* @param collectionSlug - Target collection (e.g., 'media', 'documents')
|
|
1546
|
+
* @param file - The file to upload
|
|
1547
|
+
* @param fields - Additional form fields to include (e.g., alt, title)
|
|
1548
|
+
* @returns Observable emitting upload progress
|
|
1549
|
+
*/
|
|
1550
|
+
uploadToCollection(collectionSlug, file, fields) {
|
|
1551
|
+
const subject = new Subject();
|
|
1552
|
+
const formData = new FormData();
|
|
1553
|
+
formData.append('file', file);
|
|
1554
|
+
if (fields) {
|
|
1555
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1556
|
+
formData.append(key, value);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const initialProgress = {
|
|
1560
|
+
status: 'pending',
|
|
1561
|
+
progress: 0,
|
|
1562
|
+
file,
|
|
1563
|
+
};
|
|
1564
|
+
this.updateActiveUpload(file, initialProgress);
|
|
1565
|
+
subject.next(initialProgress);
|
|
1566
|
+
const url = `/api/${collectionSlug}`;
|
|
1567
|
+
const request = new HttpRequest('POST', url, formData, {
|
|
1568
|
+
reportProgress: true,
|
|
1569
|
+
});
|
|
1570
|
+
this.http
|
|
1571
|
+
.request(request)
|
|
1572
|
+
.pipe(finalize(() => {
|
|
1573
|
+
const uploads = new Map(this.activeUploadsSignal());
|
|
1574
|
+
uploads.delete(file);
|
|
1575
|
+
this.activeUploadsSignal.set(uploads);
|
|
1576
|
+
subject.complete();
|
|
1577
|
+
}))
|
|
1578
|
+
.subscribe({
|
|
1579
|
+
next: (event) => {
|
|
1580
|
+
if (event.type === HttpEventType.UploadProgress) {
|
|
1581
|
+
const progress = event.total
|
|
1582
|
+
? Math.round((100 * event.loaded) / event.total)
|
|
1583
|
+
: 0;
|
|
1584
|
+
const uploadingProgress = {
|
|
1585
|
+
status: 'uploading',
|
|
1586
|
+
progress,
|
|
1587
|
+
file,
|
|
1588
|
+
};
|
|
1589
|
+
this.updateActiveUpload(file, uploadingProgress);
|
|
1590
|
+
subject.next(uploadingProgress);
|
|
1591
|
+
}
|
|
1592
|
+
else if (event.type === HttpEventType.Response) {
|
|
1593
|
+
const body = event.body;
|
|
1594
|
+
if (body?.doc) {
|
|
1595
|
+
const completeProgress = {
|
|
1596
|
+
status: 'complete',
|
|
1597
|
+
progress: 100,
|
|
1598
|
+
file,
|
|
1599
|
+
result: body.doc,
|
|
1600
|
+
};
|
|
1601
|
+
this.updateActiveUpload(file, completeProgress);
|
|
1602
|
+
subject.next(completeProgress);
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
const errorProgress = {
|
|
1606
|
+
status: 'error',
|
|
1607
|
+
progress: 0,
|
|
1608
|
+
file,
|
|
1609
|
+
error: body?.error ?? 'Upload failed',
|
|
1610
|
+
};
|
|
1611
|
+
this.updateActiveUpload(file, errorProgress);
|
|
1612
|
+
subject.next(errorProgress);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
error: (error) => {
|
|
1617
|
+
const errorProgress = {
|
|
1618
|
+
status: 'error',
|
|
1619
|
+
progress: 0,
|
|
1620
|
+
file,
|
|
1621
|
+
error: error.message ?? 'Upload failed',
|
|
1622
|
+
};
|
|
1623
|
+
this.updateActiveUpload(file, errorProgress);
|
|
1624
|
+
subject.next(errorProgress);
|
|
1625
|
+
subject.error(error);
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
return subject.asObservable();
|
|
1629
|
+
}
|
|
1542
1630
|
/**
|
|
1543
1631
|
* Upload multiple files.
|
|
1544
1632
|
*
|
|
@@ -3530,15 +3618,20 @@ class FieldRenderer {
|
|
|
3530
3618
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: FieldRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3531
3619
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: FieldRenderer, isStandalone: true, selector: "mcms-field-renderer", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, formNode: { classPropertyName: "formNode", publicName: "formNode", isSignal: true, isRequired: false, transformFunction: null }, formTree: { classPropertyName: "formTree", publicName: "formTree", isSignal: true, isRequired: false, transformFunction: null }, formModel: { classPropertyName: "formModel", publicName: "formModel", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
3532
3620
|
@if (resolvedComponent()) {
|
|
3533
|
-
<ng-container
|
|
3534
|
-
*ngComponentOutlet="resolvedComponent(); inputs: rendererInputs()"
|
|
3535
|
-
/>
|
|
3621
|
+
<ng-container *ngComponentOutlet="resolvedComponent(); inputs: rendererInputs()" />
|
|
3536
3622
|
} @else if (loadError()) {
|
|
3537
|
-
<div
|
|
3623
|
+
<div
|
|
3624
|
+
role="alert"
|
|
3625
|
+
class="rounded-md border border-destructive/50 bg-destructive/10 p-2 text-sm text-destructive"
|
|
3626
|
+
>
|
|
3538
3627
|
Failed to load field renderer
|
|
3539
3628
|
</div>
|
|
3540
3629
|
} @else {
|
|
3541
|
-
<div
|
|
3630
|
+
<div
|
|
3631
|
+
role="status"
|
|
3632
|
+
aria-label="Loading field"
|
|
3633
|
+
class="h-10 animate-pulse rounded-md bg-muted"
|
|
3634
|
+
></div>
|
|
3542
3635
|
}
|
|
3543
3636
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3544
3637
|
}
|
|
@@ -3551,15 +3644,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
3551
3644
|
host: { class: 'block' },
|
|
3552
3645
|
template: `
|
|
3553
3646
|
@if (resolvedComponent()) {
|
|
3554
|
-
<ng-container
|
|
3555
|
-
*ngComponentOutlet="resolvedComponent(); inputs: rendererInputs()"
|
|
3556
|
-
/>
|
|
3647
|
+
<ng-container *ngComponentOutlet="resolvedComponent(); inputs: rendererInputs()" />
|
|
3557
3648
|
} @else if (loadError()) {
|
|
3558
|
-
<div
|
|
3649
|
+
<div
|
|
3650
|
+
role="alert"
|
|
3651
|
+
class="rounded-md border border-destructive/50 bg-destructive/10 p-2 text-sm text-destructive"
|
|
3652
|
+
>
|
|
3559
3653
|
Failed to load field renderer
|
|
3560
3654
|
</div>
|
|
3561
3655
|
} @else {
|
|
3562
|
-
<div
|
|
3656
|
+
<div
|
|
3657
|
+
role="status"
|
|
3658
|
+
aria-label="Loading field"
|
|
3659
|
+
class="h-10 animate-pulse rounded-md bg-muted"
|
|
3660
|
+
></div>
|
|
3563
3661
|
}
|
|
3564
3662
|
`,
|
|
3565
3663
|
}]
|
|
@@ -4119,113 +4217,731 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4119
4217
|
}], 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"] }] } });
|
|
4120
4218
|
|
|
4121
4219
|
/**
|
|
4122
|
-
*
|
|
4220
|
+
* Media Preview Component
|
|
4123
4221
|
*
|
|
4124
|
-
*
|
|
4222
|
+
* Displays a preview of media based on its type:
|
|
4223
|
+
* - Images: Thumbnail preview
|
|
4224
|
+
* - Videos: Video icon with optional poster
|
|
4225
|
+
* - Audio: Audio icon
|
|
4226
|
+
* - Documents: Document icon
|
|
4227
|
+
* - Other: Generic file icon
|
|
4125
4228
|
*
|
|
4126
4229
|
* @example
|
|
4127
4230
|
* ```html
|
|
4128
|
-
* <mcms-
|
|
4129
|
-
* [
|
|
4130
|
-
*
|
|
4131
|
-
* mode="edit"
|
|
4132
|
-
* (saved)="onSaved($event)"
|
|
4133
|
-
* (cancelled)="onCancel()"
|
|
4231
|
+
* <mcms-media-preview
|
|
4232
|
+
* [media]="mediaDocument"
|
|
4233
|
+
* [size]="'md'"
|
|
4134
4234
|
* />
|
|
4135
4235
|
* ```
|
|
4136
4236
|
*/
|
|
4137
|
-
class
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
/**
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
dashboardPath = computed(() => {
|
|
4199
|
-
const base = this.basePath();
|
|
4200
|
-
return base.replace(/\/collections$/, '');
|
|
4201
|
-
}, ...(ngDevMode ? [{ debugName: "dashboardPath" }] : []));
|
|
4202
|
-
/** Collection list path */
|
|
4203
|
-
collectionListPath = computed(() => {
|
|
4204
|
-
return `${this.basePath()}/${this.collection().slug}`;
|
|
4205
|
-
}, ...(ngDevMode ? [{ debugName: "collectionListPath" }] : []));
|
|
4206
|
-
/** Page title for breadcrumb */
|
|
4207
|
-
pageTitle = computed(() => {
|
|
4208
|
-
if (this.isGlobal()) {
|
|
4209
|
-
return this.collectionLabelSingular();
|
|
4237
|
+
class MediaPreviewComponent {
|
|
4238
|
+
/** Media data to preview */
|
|
4239
|
+
media = input(null, ...(ngDevMode ? [{ debugName: "media" }] : []));
|
|
4240
|
+
/** Size of the preview */
|
|
4241
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
4242
|
+
/** Custom class override */
|
|
4243
|
+
class = input('', ...(ngDevMode ? [{ debugName: "class" }] : []));
|
|
4244
|
+
/** Whether to show rounded corners */
|
|
4245
|
+
rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : []));
|
|
4246
|
+
/** Host classes */
|
|
4247
|
+
hostClasses = computed(() => {
|
|
4248
|
+
const sizeClass = this.sizeClasses()[this.size()];
|
|
4249
|
+
const roundedClass = this.rounded() ? 'rounded-md overflow-hidden' : '';
|
|
4250
|
+
return `inline-block ${sizeClass} ${roundedClass} ${this.class()}`;
|
|
4251
|
+
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
4252
|
+
/** Size classes map */
|
|
4253
|
+
sizeClasses = computed(() => ({
|
|
4254
|
+
xs: 'h-8 w-8',
|
|
4255
|
+
sm: 'h-12 w-12',
|
|
4256
|
+
md: 'h-20 w-20',
|
|
4257
|
+
lg: 'h-32 w-32',
|
|
4258
|
+
xl: 'h-48 w-48',
|
|
4259
|
+
}), ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
|
|
4260
|
+
/** Icon size classes */
|
|
4261
|
+
iconClasses = computed(() => {
|
|
4262
|
+
const sizes = {
|
|
4263
|
+
xs: 'text-lg',
|
|
4264
|
+
sm: 'text-xl',
|
|
4265
|
+
md: 'text-3xl',
|
|
4266
|
+
lg: 'text-4xl',
|
|
4267
|
+
xl: 'text-6xl',
|
|
4268
|
+
};
|
|
4269
|
+
return `${sizes[this.size()]} text-mcms-muted-foreground`;
|
|
4270
|
+
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
4271
|
+
/** Whether the media is an image */
|
|
4272
|
+
isImage = computed(() => {
|
|
4273
|
+
const mimeType = this.media()?.mimeType ?? '';
|
|
4274
|
+
return mimeType.startsWith('image/');
|
|
4275
|
+
}, ...(ngDevMode ? [{ debugName: "isImage" }] : []));
|
|
4276
|
+
/** Whether the media is a video */
|
|
4277
|
+
isVideo = computed(() => {
|
|
4278
|
+
const mimeType = this.media()?.mimeType ?? '';
|
|
4279
|
+
return mimeType.startsWith('video/');
|
|
4280
|
+
}, ...(ngDevMode ? [{ debugName: "isVideo" }] : []));
|
|
4281
|
+
/** Whether the media is audio */
|
|
4282
|
+
isAudio = computed(() => {
|
|
4283
|
+
const mimeType = this.media()?.mimeType ?? '';
|
|
4284
|
+
return mimeType.startsWith('audio/');
|
|
4285
|
+
}, ...(ngDevMode ? [{ debugName: "isAudio" }] : []));
|
|
4286
|
+
/** Image URL for preview */
|
|
4287
|
+
imageUrl = computed(() => {
|
|
4288
|
+
const media = this.media();
|
|
4289
|
+
if (!media)
|
|
4290
|
+
return '';
|
|
4291
|
+
return media.url ?? `/api/media/file/${media.path}`;
|
|
4292
|
+
}, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
|
|
4293
|
+
/** Icon name based on media type */
|
|
4294
|
+
iconName = computed(() => {
|
|
4295
|
+
const mimeType = this.media()?.mimeType ?? '';
|
|
4296
|
+
if (mimeType.startsWith('video/')) {
|
|
4297
|
+
return heroFilm;
|
|
4210
4298
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
return `Create ${this.collectionLabelSingular()}`;
|
|
4299
|
+
if (mimeType.startsWith('audio/')) {
|
|
4300
|
+
return heroMusicalNote;
|
|
4214
4301
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
for (const field of titleFields) {
|
|
4218
|
-
if (data[field] && typeof data[field] === 'string') {
|
|
4219
|
-
return data[field];
|
|
4220
|
-
}
|
|
4302
|
+
if (mimeType === 'application/pdf') {
|
|
4303
|
+
return heroDocumentText;
|
|
4221
4304
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
return
|
|
4305
|
+
if (mimeType.startsWith('application/zip') || mimeType.includes('compressed')) {
|
|
4306
|
+
return heroArchiveBox;
|
|
4307
|
+
}
|
|
4308
|
+
if (mimeType.startsWith('image/')) {
|
|
4309
|
+
return heroPhoto;
|
|
4310
|
+
}
|
|
4311
|
+
return heroDocument;
|
|
4312
|
+
}, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
|
|
4313
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4314
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", 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: `
|
|
4315
|
+
@if (isImage()) {
|
|
4316
|
+
<img
|
|
4317
|
+
[src]="imageUrl()"
|
|
4318
|
+
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
4319
|
+
class="h-full w-full object-cover"
|
|
4320
|
+
/>
|
|
4321
|
+
} @else {
|
|
4322
|
+
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
4323
|
+
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
4324
|
+
</div>
|
|
4325
|
+
}
|
|
4326
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4327
|
+
}
|
|
4328
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, decorators: [{
|
|
4329
|
+
type: Component,
|
|
4330
|
+
args: [{
|
|
4331
|
+
selector: 'mcms-media-preview',
|
|
4332
|
+
host: {
|
|
4333
|
+
'[class]': 'hostClasses()',
|
|
4334
|
+
},
|
|
4335
|
+
template: `
|
|
4336
|
+
@if (isImage()) {
|
|
4337
|
+
<img
|
|
4338
|
+
[src]="imageUrl()"
|
|
4339
|
+
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
4340
|
+
class="h-full w-full object-cover"
|
|
4341
|
+
/>
|
|
4342
|
+
} @else {
|
|
4343
|
+
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
4344
|
+
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
4345
|
+
</div>
|
|
4346
|
+
}
|
|
4347
|
+
`,
|
|
4348
|
+
imports: [NgIcon],
|
|
4349
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4350
|
+
}]
|
|
4351
|
+
}], propDecorators: { media: [{ type: i0.Input, args: [{ isSignal: true, alias: "media", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }] } });
|
|
4352
|
+
|
|
4353
|
+
/**
|
|
4354
|
+
* Upload zone component for upload collections.
|
|
4355
|
+
* Shows a drag-and-drop zone above the form fields.
|
|
4356
|
+
* When a file is selected, emits the file for the parent entity form to handle.
|
|
4357
|
+
*/
|
|
4358
|
+
/**
|
|
4359
|
+
* Get string property from an unknown object.
|
|
4360
|
+
*/
|
|
4361
|
+
function getStringProp$1(obj, key) {
|
|
4362
|
+
if (typeof obj !== 'object' || obj === null)
|
|
4363
|
+
return undefined;
|
|
4364
|
+
if (!(key in obj))
|
|
4365
|
+
return undefined;
|
|
4366
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4367
|
+
const value = obj[key]; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
4368
|
+
return typeof value === 'string' ? value : undefined;
|
|
4369
|
+
}
|
|
4370
|
+
/**
|
|
4371
|
+
* Get HTMLInputElement safely from event.
|
|
4372
|
+
*/
|
|
4373
|
+
function getInputFromEvent$1(event) {
|
|
4374
|
+
const target = event.target;
|
|
4375
|
+
if (target instanceof HTMLInputElement) {
|
|
4376
|
+
return target;
|
|
4377
|
+
}
|
|
4378
|
+
return null;
|
|
4379
|
+
}
|
|
4380
|
+
class CollectionUploadZoneComponent {
|
|
4381
|
+
/** Reference to the hidden file input */
|
|
4382
|
+
fileInputRef = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInputRef" }] : []));
|
|
4383
|
+
/** Upload config from the collection */
|
|
4384
|
+
uploadConfig = input(undefined, ...(ngDevMode ? [{ debugName: "uploadConfig" }] : []));
|
|
4385
|
+
/** Whether the zone is disabled */
|
|
4386
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
4387
|
+
/** Currently selected file (read from parent) */
|
|
4388
|
+
pendingFile = input(null, ...(ngDevMode ? [{ debugName: "pendingFile" }] : []));
|
|
4389
|
+
/** Whether an upload is in progress */
|
|
4390
|
+
isUploading = input(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
4391
|
+
/** Upload progress percentage */
|
|
4392
|
+
uploadProgress = input(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
|
|
4393
|
+
/** Error message */
|
|
4394
|
+
error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
4395
|
+
/** Existing media data for edit mode (filename, mimeType, path, url) */
|
|
4396
|
+
existingMedia = input(null, ...(ngDevMode ? [{ debugName: "existingMedia" }] : []));
|
|
4397
|
+
/** Emitted when a file is selected */
|
|
4398
|
+
fileSelected = output();
|
|
4399
|
+
/** Emitted when the file is removed */
|
|
4400
|
+
fileRemoved = output();
|
|
4401
|
+
/** Icons */
|
|
4402
|
+
uploadIcon = heroCloudArrowUp;
|
|
4403
|
+
xMarkIcon = heroXMark;
|
|
4404
|
+
/** Drag state */
|
|
4405
|
+
isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
|
|
4406
|
+
/** Cached object URL for file preview (managed to prevent memory leaks) */
|
|
4407
|
+
previewUrl = signal(null, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
|
|
4408
|
+
constructor() {
|
|
4409
|
+
// Manage object URL lifecycle: create when file changes, revoke old one
|
|
4410
|
+
effect((onCleanup) => {
|
|
4411
|
+
const file = this.pendingFile();
|
|
4412
|
+
if (file) {
|
|
4413
|
+
const url = URL.createObjectURL(file);
|
|
4414
|
+
this.previewUrl.set(url);
|
|
4415
|
+
onCleanup(() => URL.revokeObjectURL(url));
|
|
4416
|
+
}
|
|
4417
|
+
else {
|
|
4418
|
+
this.previewUrl.set(null);
|
|
4419
|
+
}
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
/** Preview data for the pending file */
|
|
4423
|
+
previewData = computed(() => {
|
|
4424
|
+
const file = this.pendingFile();
|
|
4425
|
+
if (!file)
|
|
4426
|
+
return null;
|
|
4427
|
+
return {
|
|
4428
|
+
mimeType: file.type,
|
|
4429
|
+
filename: file.name,
|
|
4430
|
+
url: this.previewUrl() ?? undefined,
|
|
4431
|
+
};
|
|
4432
|
+
}, ...(ngDevMode ? [{ debugName: "previewData" }] : []));
|
|
4433
|
+
/** Preview data for existing media (edit mode) */
|
|
4434
|
+
existingMediaPreview = computed(() => {
|
|
4435
|
+
const media = this.existingMedia();
|
|
4436
|
+
if (!media)
|
|
4437
|
+
return null;
|
|
4438
|
+
return {
|
|
4439
|
+
url: getStringProp$1(media, 'url'),
|
|
4440
|
+
path: getStringProp$1(media, 'path'),
|
|
4441
|
+
mimeType: getStringProp$1(media, 'mimeType'),
|
|
4442
|
+
filename: getStringProp$1(media, 'filename'),
|
|
4443
|
+
alt: getStringProp$1(media, 'alt'),
|
|
4444
|
+
};
|
|
4445
|
+
}, ...(ngDevMode ? [{ debugName: "existingMediaPreview" }] : []));
|
|
4446
|
+
/** Filename of existing media */
|
|
4447
|
+
existingFilename = computed(() => {
|
|
4448
|
+
return getStringProp$1(this.existingMedia(), 'filename') ?? 'Uploaded file';
|
|
4449
|
+
}, ...(ngDevMode ? [{ debugName: "existingFilename" }] : []));
|
|
4450
|
+
/** MIME type of existing media */
|
|
4451
|
+
existingMimeType = computed(() => {
|
|
4452
|
+
return getStringProp$1(this.existingMedia(), 'mimeType') ?? '';
|
|
4453
|
+
}, ...(ngDevMode ? [{ debugName: "existingMimeType" }] : []));
|
|
4454
|
+
/** MIME types hint for display */
|
|
4455
|
+
mimeTypesHint = computed(() => {
|
|
4456
|
+
const mimeTypes = this.uploadConfig()?.mimeTypes;
|
|
4457
|
+
if (!mimeTypes || mimeTypes.length === 0)
|
|
4458
|
+
return null;
|
|
4459
|
+
const simplified = mimeTypes.map((type) => {
|
|
4460
|
+
if (type === 'image/*')
|
|
4461
|
+
return 'Images';
|
|
4462
|
+
if (type === 'video/*')
|
|
4463
|
+
return 'Videos';
|
|
4464
|
+
if (type === 'audio/*')
|
|
4465
|
+
return 'Audio';
|
|
4466
|
+
if (type === 'application/pdf')
|
|
4467
|
+
return 'PDF';
|
|
4468
|
+
return type;
|
|
4469
|
+
});
|
|
4470
|
+
return `Allowed: ${simplified.join(', ')}`;
|
|
4471
|
+
}, ...(ngDevMode ? [{ debugName: "mimeTypesHint" }] : []));
|
|
4472
|
+
/** Max size hint for display */
|
|
4473
|
+
maxSizeHint = computed(() => {
|
|
4474
|
+
const maxSize = this.uploadConfig()?.maxFileSize;
|
|
4475
|
+
if (!maxSize)
|
|
4476
|
+
return null;
|
|
4477
|
+
if (maxSize >= 1024 * 1024 * 1024) {
|
|
4478
|
+
return `${(maxSize / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
4479
|
+
}
|
|
4480
|
+
if (maxSize >= 1024 * 1024) {
|
|
4481
|
+
return `${(maxSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
4482
|
+
}
|
|
4483
|
+
if (maxSize >= 1024) {
|
|
4484
|
+
return `${(maxSize / 1024).toFixed(1)} KB`;
|
|
4485
|
+
}
|
|
4486
|
+
return `${maxSize} bytes`;
|
|
4487
|
+
}, ...(ngDevMode ? [{ debugName: "maxSizeHint" }] : []));
|
|
4488
|
+
/** Accept attribute for file input */
|
|
4489
|
+
acceptAttribute = computed(() => {
|
|
4490
|
+
const mimeTypes = this.uploadConfig()?.mimeTypes;
|
|
4491
|
+
if (!mimeTypes || mimeTypes.length === 0)
|
|
4492
|
+
return '*/*';
|
|
4493
|
+
return mimeTypes.join(',');
|
|
4494
|
+
}, ...(ngDevMode ? [{ debugName: "acceptAttribute" }] : []));
|
|
4495
|
+
/**
|
|
4496
|
+
* Format file size for display.
|
|
4497
|
+
*/
|
|
4498
|
+
formatFileSize(bytes) {
|
|
4499
|
+
if (bytes >= 1024 * 1024 * 1024) {
|
|
4500
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
4501
|
+
}
|
|
4502
|
+
if (bytes >= 1024 * 1024) {
|
|
4503
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
4504
|
+
}
|
|
4505
|
+
if (bytes >= 1024) {
|
|
4506
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4507
|
+
}
|
|
4508
|
+
return `${bytes} bytes`;
|
|
4509
|
+
}
|
|
4510
|
+
/**
|
|
4511
|
+
* Handle drag over event.
|
|
4512
|
+
*/
|
|
4513
|
+
onDragOver(event) {
|
|
4514
|
+
event.preventDefault();
|
|
4515
|
+
event.stopPropagation();
|
|
4516
|
+
if (!this.disabled()) {
|
|
4517
|
+
this.isDragging.set(true);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Handle drag leave event.
|
|
4522
|
+
*/
|
|
4523
|
+
onDragLeave(event) {
|
|
4524
|
+
event.preventDefault();
|
|
4525
|
+
event.stopPropagation();
|
|
4526
|
+
this.isDragging.set(false);
|
|
4527
|
+
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Handle file drop.
|
|
4530
|
+
*/
|
|
4531
|
+
onDrop(event) {
|
|
4532
|
+
event.preventDefault();
|
|
4533
|
+
event.stopPropagation();
|
|
4534
|
+
this.isDragging.set(false);
|
|
4535
|
+
if (this.disabled())
|
|
4536
|
+
return;
|
|
4537
|
+
const files = event.dataTransfer?.files;
|
|
4538
|
+
if (files && files.length > 0) {
|
|
4539
|
+
this.fileSelected.emit(files[0]);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
/**
|
|
4543
|
+
* Trigger hidden file input click.
|
|
4544
|
+
*/
|
|
4545
|
+
triggerFileInput() {
|
|
4546
|
+
if (this.disabled())
|
|
4547
|
+
return;
|
|
4548
|
+
const ref = this.fileInputRef();
|
|
4549
|
+
if (ref) {
|
|
4550
|
+
ref.nativeElement.click();
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* Handle file selection from input.
|
|
4555
|
+
*/
|
|
4556
|
+
onFileSelected(event) {
|
|
4557
|
+
const input = getInputFromEvent$1(event);
|
|
4558
|
+
if (!input)
|
|
4559
|
+
return;
|
|
4560
|
+
const files = input.files;
|
|
4561
|
+
if (files && files.length > 0) {
|
|
4562
|
+
this.fileSelected.emit(files[0]);
|
|
4563
|
+
}
|
|
4564
|
+
// Reset input so same file can be selected again
|
|
4565
|
+
input.value = '';
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Remove the pending file.
|
|
4569
|
+
*/
|
|
4570
|
+
removeFile() {
|
|
4571
|
+
this.fileRemoved.emit();
|
|
4572
|
+
}
|
|
4573
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionUploadZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4574
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: CollectionUploadZoneComponent, isStandalone: true, selector: "mcms-collection-upload-zone", inputs: { uploadConfig: { classPropertyName: "uploadConfig", publicName: "uploadConfig", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, pendingFile: { classPropertyName: "pendingFile", publicName: "pendingFile", isSignal: true, isRequired: false, transformFunction: null }, isUploading: { classPropertyName: "isUploading", publicName: "isUploading", isSignal: true, isRequired: false, transformFunction: null }, uploadProgress: { classPropertyName: "uploadProgress", publicName: "uploadProgress", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, existingMedia: { classPropertyName: "existingMedia", publicName: "existingMedia", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", fileRemoved: "fileRemoved" }, host: { classAttribute: "block mb-6" }, viewQueries: [{ propertyName: "fileInputRef", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
4575
|
+
@if (pendingFile()) {
|
|
4576
|
+
<!-- File selected preview -->
|
|
4577
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card p-4">
|
|
4578
|
+
<div class="flex items-center gap-4">
|
|
4579
|
+
<mcms-media-preview [media]="previewData()" size="lg" />
|
|
4580
|
+
<div class="min-w-0 flex-1">
|
|
4581
|
+
<p class="truncate text-sm font-medium">{{ pendingFile()!.name }}</p>
|
|
4582
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
4583
|
+
{{ formatFileSize(pendingFile()!.size) }} · {{ pendingFile()!.type }}
|
|
4584
|
+
</p>
|
|
4585
|
+
@if (isUploading()) {
|
|
4586
|
+
<mcms-progress [value]="uploadProgress()" class="mt-2" />
|
|
4587
|
+
<p class="mt-1 text-xs text-mcms-muted-foreground">
|
|
4588
|
+
{{ uploadProgress() }}% uploaded
|
|
4589
|
+
</p>
|
|
4590
|
+
}
|
|
4591
|
+
</div>
|
|
4592
|
+
@if (!isUploading()) {
|
|
4593
|
+
<button
|
|
4594
|
+
mcms-button
|
|
4595
|
+
variant="ghost"
|
|
4596
|
+
size="sm"
|
|
4597
|
+
type="button"
|
|
4598
|
+
(click)="removeFile()"
|
|
4599
|
+
aria-label="Remove selected file"
|
|
4600
|
+
>
|
|
4601
|
+
<ng-icon [name]="xMarkIcon" class="h-4 w-4" />
|
|
4602
|
+
</button>
|
|
4603
|
+
}
|
|
4604
|
+
</div>
|
|
4605
|
+
</div>
|
|
4606
|
+
} @else if (existingMediaPreview()) {
|
|
4607
|
+
<!-- Existing file preview (edit mode) -->
|
|
4608
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card p-4">
|
|
4609
|
+
<div class="flex items-center gap-4">
|
|
4610
|
+
<mcms-media-preview [media]="existingMediaPreview()" size="lg" />
|
|
4611
|
+
<div class="min-w-0 flex-1">
|
|
4612
|
+
<p class="truncate text-sm font-medium">{{ existingFilename() }}</p>
|
|
4613
|
+
<p class="text-xs text-mcms-muted-foreground">{{ existingMimeType() }}</p>
|
|
4614
|
+
</div>
|
|
4615
|
+
@if (!disabled()) {
|
|
4616
|
+
<button
|
|
4617
|
+
mcms-button
|
|
4618
|
+
variant="ghost"
|
|
4619
|
+
size="sm"
|
|
4620
|
+
type="button"
|
|
4621
|
+
(click)="triggerFileInput()"
|
|
4622
|
+
aria-label="Replace file"
|
|
4623
|
+
>
|
|
4624
|
+
Replace
|
|
4625
|
+
</button>
|
|
4626
|
+
}
|
|
4627
|
+
</div>
|
|
4628
|
+
<input
|
|
4629
|
+
#fileInput
|
|
4630
|
+
type="file"
|
|
4631
|
+
class="sr-only"
|
|
4632
|
+
[accept]="acceptAttribute()"
|
|
4633
|
+
[disabled]="disabled()"
|
|
4634
|
+
(change)="onFileSelected($event)"
|
|
4635
|
+
aria-label="Choose file to upload"
|
|
4636
|
+
/>
|
|
4637
|
+
</div>
|
|
4638
|
+
} @else {
|
|
4639
|
+
<!-- Drop zone -->
|
|
4640
|
+
<div
|
|
4641
|
+
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
4642
|
+
[class.border-mcms-border]="!isDragging()"
|
|
4643
|
+
[class.border-mcms-primary]="isDragging()"
|
|
4644
|
+
[class.bg-mcms-primary/5]="isDragging()"
|
|
4645
|
+
[class.cursor-pointer]="!disabled()"
|
|
4646
|
+
[class.opacity-50]="disabled()"
|
|
4647
|
+
tabindex="0"
|
|
4648
|
+
role="button"
|
|
4649
|
+
[attr.aria-disabled]="disabled()"
|
|
4650
|
+
aria-label="Upload file. Drag and drop or click to browse."
|
|
4651
|
+
(dragover)="onDragOver($event)"
|
|
4652
|
+
(dragleave)="onDragLeave($event)"
|
|
4653
|
+
(drop)="onDrop($event)"
|
|
4654
|
+
(click)="triggerFileInput()"
|
|
4655
|
+
(keydown.enter)="triggerFileInput()"
|
|
4656
|
+
(keydown.space)="triggerFileInput()"
|
|
4657
|
+
>
|
|
4658
|
+
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
4659
|
+
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
4660
|
+
<div class="text-center">
|
|
4661
|
+
<p class="text-sm font-medium">
|
|
4662
|
+
@if (isDragging()) {
|
|
4663
|
+
Drop file here
|
|
4664
|
+
} @else {
|
|
4665
|
+
Drag & drop or click to upload
|
|
4666
|
+
}
|
|
4667
|
+
</p>
|
|
4668
|
+
@if (mimeTypesHint()) {
|
|
4669
|
+
<p class="mt-1 text-xs text-mcms-muted-foreground">
|
|
4670
|
+
{{ mimeTypesHint() }}
|
|
4671
|
+
</p>
|
|
4672
|
+
}
|
|
4673
|
+
@if (maxSizeHint()) {
|
|
4674
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
4675
|
+
Max size: {{ maxSizeHint() }}
|
|
4676
|
+
</p>
|
|
4677
|
+
}
|
|
4678
|
+
</div>
|
|
4679
|
+
</div>
|
|
4680
|
+
<input
|
|
4681
|
+
#fileInput
|
|
4682
|
+
type="file"
|
|
4683
|
+
class="sr-only"
|
|
4684
|
+
[accept]="acceptAttribute()"
|
|
4685
|
+
[disabled]="disabled()"
|
|
4686
|
+
(change)="onFileSelected($event)"
|
|
4687
|
+
aria-label="Choose file to upload"
|
|
4688
|
+
/>
|
|
4689
|
+
</div>
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
@if (error()) {
|
|
4693
|
+
<p class="mt-1 text-sm text-mcms-destructive">{{ error() }}</p>
|
|
4694
|
+
}
|
|
4695
|
+
`, isInline: true, dependencies: [{ kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Progress, selector: "mcms-progress", inputs: ["value", "max", "class"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "component", type: MediaPreviewComponent, selector: "mcms-media-preview", inputs: ["media", "size", "class", "rounded"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4696
|
+
}
|
|
4697
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionUploadZoneComponent, decorators: [{
|
|
4698
|
+
type: Component,
|
|
4699
|
+
args: [{
|
|
4700
|
+
selector: 'mcms-collection-upload-zone',
|
|
4701
|
+
imports: [Button, Progress, NgIcon, MediaPreviewComponent],
|
|
4702
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4703
|
+
host: { class: 'block mb-6' },
|
|
4704
|
+
template: `
|
|
4705
|
+
@if (pendingFile()) {
|
|
4706
|
+
<!-- File selected preview -->
|
|
4707
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card p-4">
|
|
4708
|
+
<div class="flex items-center gap-4">
|
|
4709
|
+
<mcms-media-preview [media]="previewData()" size="lg" />
|
|
4710
|
+
<div class="min-w-0 flex-1">
|
|
4711
|
+
<p class="truncate text-sm font-medium">{{ pendingFile()!.name }}</p>
|
|
4712
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
4713
|
+
{{ formatFileSize(pendingFile()!.size) }} · {{ pendingFile()!.type }}
|
|
4714
|
+
</p>
|
|
4715
|
+
@if (isUploading()) {
|
|
4716
|
+
<mcms-progress [value]="uploadProgress()" class="mt-2" />
|
|
4717
|
+
<p class="mt-1 text-xs text-mcms-muted-foreground">
|
|
4718
|
+
{{ uploadProgress() }}% uploaded
|
|
4719
|
+
</p>
|
|
4720
|
+
}
|
|
4721
|
+
</div>
|
|
4722
|
+
@if (!isUploading()) {
|
|
4723
|
+
<button
|
|
4724
|
+
mcms-button
|
|
4725
|
+
variant="ghost"
|
|
4726
|
+
size="sm"
|
|
4727
|
+
type="button"
|
|
4728
|
+
(click)="removeFile()"
|
|
4729
|
+
aria-label="Remove selected file"
|
|
4730
|
+
>
|
|
4731
|
+
<ng-icon [name]="xMarkIcon" class="h-4 w-4" />
|
|
4732
|
+
</button>
|
|
4733
|
+
}
|
|
4734
|
+
</div>
|
|
4735
|
+
</div>
|
|
4736
|
+
} @else if (existingMediaPreview()) {
|
|
4737
|
+
<!-- Existing file preview (edit mode) -->
|
|
4738
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card p-4">
|
|
4739
|
+
<div class="flex items-center gap-4">
|
|
4740
|
+
<mcms-media-preview [media]="existingMediaPreview()" size="lg" />
|
|
4741
|
+
<div class="min-w-0 flex-1">
|
|
4742
|
+
<p class="truncate text-sm font-medium">{{ existingFilename() }}</p>
|
|
4743
|
+
<p class="text-xs text-mcms-muted-foreground">{{ existingMimeType() }}</p>
|
|
4744
|
+
</div>
|
|
4745
|
+
@if (!disabled()) {
|
|
4746
|
+
<button
|
|
4747
|
+
mcms-button
|
|
4748
|
+
variant="ghost"
|
|
4749
|
+
size="sm"
|
|
4750
|
+
type="button"
|
|
4751
|
+
(click)="triggerFileInput()"
|
|
4752
|
+
aria-label="Replace file"
|
|
4753
|
+
>
|
|
4754
|
+
Replace
|
|
4755
|
+
</button>
|
|
4756
|
+
}
|
|
4757
|
+
</div>
|
|
4758
|
+
<input
|
|
4759
|
+
#fileInput
|
|
4760
|
+
type="file"
|
|
4761
|
+
class="sr-only"
|
|
4762
|
+
[accept]="acceptAttribute()"
|
|
4763
|
+
[disabled]="disabled()"
|
|
4764
|
+
(change)="onFileSelected($event)"
|
|
4765
|
+
aria-label="Choose file to upload"
|
|
4766
|
+
/>
|
|
4767
|
+
</div>
|
|
4768
|
+
} @else {
|
|
4769
|
+
<!-- Drop zone -->
|
|
4770
|
+
<div
|
|
4771
|
+
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
4772
|
+
[class.border-mcms-border]="!isDragging()"
|
|
4773
|
+
[class.border-mcms-primary]="isDragging()"
|
|
4774
|
+
[class.bg-mcms-primary/5]="isDragging()"
|
|
4775
|
+
[class.cursor-pointer]="!disabled()"
|
|
4776
|
+
[class.opacity-50]="disabled()"
|
|
4777
|
+
tabindex="0"
|
|
4778
|
+
role="button"
|
|
4779
|
+
[attr.aria-disabled]="disabled()"
|
|
4780
|
+
aria-label="Upload file. Drag and drop or click to browse."
|
|
4781
|
+
(dragover)="onDragOver($event)"
|
|
4782
|
+
(dragleave)="onDragLeave($event)"
|
|
4783
|
+
(drop)="onDrop($event)"
|
|
4784
|
+
(click)="triggerFileInput()"
|
|
4785
|
+
(keydown.enter)="triggerFileInput()"
|
|
4786
|
+
(keydown.space)="triggerFileInput()"
|
|
4787
|
+
>
|
|
4788
|
+
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
4789
|
+
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
4790
|
+
<div class="text-center">
|
|
4791
|
+
<p class="text-sm font-medium">
|
|
4792
|
+
@if (isDragging()) {
|
|
4793
|
+
Drop file here
|
|
4794
|
+
} @else {
|
|
4795
|
+
Drag & drop or click to upload
|
|
4796
|
+
}
|
|
4797
|
+
</p>
|
|
4798
|
+
@if (mimeTypesHint()) {
|
|
4799
|
+
<p class="mt-1 text-xs text-mcms-muted-foreground">
|
|
4800
|
+
{{ mimeTypesHint() }}
|
|
4801
|
+
</p>
|
|
4802
|
+
}
|
|
4803
|
+
@if (maxSizeHint()) {
|
|
4804
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
4805
|
+
Max size: {{ maxSizeHint() }}
|
|
4806
|
+
</p>
|
|
4807
|
+
}
|
|
4808
|
+
</div>
|
|
4809
|
+
</div>
|
|
4810
|
+
<input
|
|
4811
|
+
#fileInput
|
|
4812
|
+
type="file"
|
|
4813
|
+
class="sr-only"
|
|
4814
|
+
[accept]="acceptAttribute()"
|
|
4815
|
+
[disabled]="disabled()"
|
|
4816
|
+
(change)="onFileSelected($event)"
|
|
4817
|
+
aria-label="Choose file to upload"
|
|
4818
|
+
/>
|
|
4819
|
+
</div>
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
@if (error()) {
|
|
4823
|
+
<p class="mt-1 text-sm text-mcms-destructive">{{ error() }}</p>
|
|
4824
|
+
}
|
|
4825
|
+
`,
|
|
4826
|
+
}]
|
|
4827
|
+
}], ctorParameters: () => [], propDecorators: { fileInputRef: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], uploadConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadConfig", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], pendingFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingFile", required: false }] }], isUploading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isUploading", required: false }] }], uploadProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadProgress", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], existingMedia: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingMedia", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }] } });
|
|
4828
|
+
|
|
4829
|
+
/**
|
|
4830
|
+
* Entity Form Widget
|
|
4831
|
+
*
|
|
4832
|
+
* Dynamic form for create/edit operations using Angular Signal Forms.
|
|
4833
|
+
*
|
|
4834
|
+
* @example
|
|
4835
|
+
* ```html
|
|
4836
|
+
* <mcms-entity-form
|
|
4837
|
+
* [collection]="postsCollection"
|
|
4838
|
+
* entityId="123"
|
|
4839
|
+
* mode="edit"
|
|
4840
|
+
* (saved)="onSaved($event)"
|
|
4841
|
+
* (cancelled)="onCancel()"
|
|
4842
|
+
* />
|
|
4843
|
+
* ```
|
|
4844
|
+
*/
|
|
4845
|
+
class EntityFormWidget {
|
|
4846
|
+
api = injectMomentumAPI();
|
|
4847
|
+
injector = inject(Injector);
|
|
4848
|
+
uploadService = inject(UploadService);
|
|
4849
|
+
versionService = inject(VersionService);
|
|
4850
|
+
collectionAccess = inject(CollectionAccessService);
|
|
4851
|
+
feedback = inject(FeedbackService);
|
|
4852
|
+
router = inject(Router);
|
|
4853
|
+
liveAnnouncer = inject(LiveAnnouncer);
|
|
4854
|
+
/** The collection configuration */
|
|
4855
|
+
collection = input.required(...(ngDevMode ? [{ debugName: "collection" }] : []));
|
|
4856
|
+
/** Entity ID for edit mode (undefined for create) */
|
|
4857
|
+
entityId = input(undefined, ...(ngDevMode ? [{ debugName: "entityId" }] : []));
|
|
4858
|
+
/** Form mode */
|
|
4859
|
+
mode = input('create', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
4860
|
+
/** Base path for navigation */
|
|
4861
|
+
basePath = input('/admin/collections', ...(ngDevMode ? [{ debugName: "basePath" }] : []));
|
|
4862
|
+
/** Whether to show breadcrumbs */
|
|
4863
|
+
showBreadcrumbs = input(true, ...(ngDevMode ? [{ debugName: "showBreadcrumbs" }] : []));
|
|
4864
|
+
/** When true, prevents router navigation after save/cancel (used in entity sheet) */
|
|
4865
|
+
suppressNavigation = input(false, ...(ngDevMode ? [{ debugName: "suppressNavigation" }] : []));
|
|
4866
|
+
/** When true, uses the global API instead of collection API (singleton mode) */
|
|
4867
|
+
isGlobal = input(false, ...(ngDevMode ? [{ debugName: "isGlobal" }] : []));
|
|
4868
|
+
/** The global slug (used when isGlobal is true) */
|
|
4869
|
+
globalSlug = input(undefined, ...(ngDevMode ? [{ debugName: "globalSlug" }] : []));
|
|
4870
|
+
/** Outputs */
|
|
4871
|
+
saved = output();
|
|
4872
|
+
cancelled = output();
|
|
4873
|
+
saveError = output();
|
|
4874
|
+
modeChange = output();
|
|
4875
|
+
draftSaved = output();
|
|
4876
|
+
/** Model signal — the single source of truth for form data */
|
|
4877
|
+
formModel = signal({}, ...(ngDevMode ? [{ debugName: "formModel" }] : []));
|
|
4878
|
+
/** Alias for backward compatibility (CollectionEditPage reads formData) */
|
|
4879
|
+
formData = this.formModel;
|
|
4880
|
+
/** Signal forms tree — created once when collection is available */
|
|
4881
|
+
entityForm = signal(null, ...(ngDevMode ? [{ debugName: "entityForm" }] : []));
|
|
4882
|
+
/** Original data for edit mode */
|
|
4883
|
+
originalData = signal(null, ...(ngDevMode ? [{ debugName: "originalData" }] : []));
|
|
4884
|
+
/** UI state */
|
|
4885
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
4886
|
+
isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : []));
|
|
4887
|
+
isSavingDraft = signal(false, ...(ngDevMode ? [{ debugName: "isSavingDraft" }] : []));
|
|
4888
|
+
formError = signal(null, ...(ngDevMode ? [{ debugName: "formError" }] : []));
|
|
4889
|
+
/** Upload collection state */
|
|
4890
|
+
pendingFile = signal(null, ...(ngDevMode ? [{ debugName: "pendingFile" }] : []));
|
|
4891
|
+
isUploadingFile = signal(false, ...(ngDevMode ? [{ debugName: "isUploadingFile" }] : []));
|
|
4892
|
+
uploadFileProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadFileProgress" }] : []));
|
|
4893
|
+
uploadFileError = signal(null, ...(ngDevMode ? [{ debugName: "uploadFileError" }] : []));
|
|
4894
|
+
/** Whether the collection is an upload collection */
|
|
4895
|
+
isUploadCol = computed(() => isUploadCollection(this.collection()), ...(ngDevMode ? [{ debugName: "isUploadCol" }] : []));
|
|
4896
|
+
/** Whether the form has been set up */
|
|
4897
|
+
formCreated = false;
|
|
4898
|
+
/** Whether the form has unsaved changes (from signal forms dirty tracking) */
|
|
4899
|
+
isDirty = computed(() => {
|
|
4900
|
+
const ef = this.entityForm();
|
|
4901
|
+
return ef ? ef().dirty() : false;
|
|
4902
|
+
}, ...(ngDevMode ? [{ debugName: "isDirty" }] : []));
|
|
4903
|
+
/** Computed collection label */
|
|
4904
|
+
collectionLabel = computed(() => {
|
|
4905
|
+
const col = this.collection();
|
|
4906
|
+
return humanizeFieldName(col.labels?.plural || col.slug);
|
|
4907
|
+
}, ...(ngDevMode ? [{ debugName: "collectionLabel" }] : []));
|
|
4908
|
+
/** Computed collection label singular */
|
|
4909
|
+
collectionLabelSingular = computed(() => {
|
|
4910
|
+
const col = this.collection();
|
|
4911
|
+
return humanizeFieldName(col.labels?.singular || col.slug);
|
|
4912
|
+
}, ...(ngDevMode ? [{ debugName: "collectionLabelSingular" }] : []));
|
|
4913
|
+
/** Dashboard path (remove /collections from base path) */
|
|
4914
|
+
dashboardPath = computed(() => {
|
|
4915
|
+
const base = this.basePath();
|
|
4916
|
+
return base.replace(/\/collections$/, '');
|
|
4917
|
+
}, ...(ngDevMode ? [{ debugName: "dashboardPath" }] : []));
|
|
4918
|
+
/** Collection list path */
|
|
4919
|
+
collectionListPath = computed(() => {
|
|
4920
|
+
return `${this.basePath()}/${this.collection().slug}`;
|
|
4921
|
+
}, ...(ngDevMode ? [{ debugName: "collectionListPath" }] : []));
|
|
4922
|
+
/** Page title for breadcrumb */
|
|
4923
|
+
pageTitle = computed(() => {
|
|
4924
|
+
if (this.isGlobal()) {
|
|
4925
|
+
return this.collectionLabelSingular();
|
|
4926
|
+
}
|
|
4927
|
+
const currentMode = this.mode();
|
|
4928
|
+
if (currentMode === 'create') {
|
|
4929
|
+
return `Create ${this.collectionLabelSingular()}`;
|
|
4930
|
+
}
|
|
4931
|
+
const data = this.formModel();
|
|
4932
|
+
const titleFields = ['title', 'name', 'label', 'subject'];
|
|
4933
|
+
for (const field of titleFields) {
|
|
4934
|
+
if (data[field] && typeof data[field] === 'string') {
|
|
4935
|
+
return data[field];
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
return `Edit ${this.collectionLabelSingular()}`;
|
|
4939
|
+
}, ...(ngDevMode ? [{ debugName: "pageTitle" }] : []));
|
|
4940
|
+
/** Visible fields (excluding hidden ones and those failing admin.condition) */
|
|
4941
|
+
visibleFields = computed(() => {
|
|
4942
|
+
const col = this.collection();
|
|
4943
|
+
const data = this.formModel();
|
|
4944
|
+
return col.fields.filter((field) => {
|
|
4229
4945
|
if (field.admin?.hidden)
|
|
4230
4946
|
return false;
|
|
4231
4947
|
if (field.admin?.condition && !field.admin.condition(data))
|
|
@@ -4354,9 +5070,62 @@ class EntityFormWidget {
|
|
|
4354
5070
|
this.isLoading.set(false);
|
|
4355
5071
|
}
|
|
4356
5072
|
}
|
|
5073
|
+
/**
|
|
5074
|
+
* Handle file selected in the upload zone.
|
|
5075
|
+
* Auto-populates metadata fields in the form model.
|
|
5076
|
+
*/
|
|
5077
|
+
onFileSelected(file) {
|
|
5078
|
+
this.pendingFile.set(file);
|
|
5079
|
+
this.uploadFileError.set(null);
|
|
5080
|
+
// Validate file against collection upload config
|
|
5081
|
+
const uploadConfig = this.collection().upload;
|
|
5082
|
+
if (uploadConfig) {
|
|
5083
|
+
// Validate size
|
|
5084
|
+
if (uploadConfig.maxFileSize && file.size > uploadConfig.maxFileSize) {
|
|
5085
|
+
this.uploadFileError.set(`File size exceeds maximum allowed size`);
|
|
5086
|
+
this.pendingFile.set(null);
|
|
5087
|
+
return;
|
|
5088
|
+
}
|
|
5089
|
+
// Validate MIME type
|
|
5090
|
+
if (uploadConfig.mimeTypes && uploadConfig.mimeTypes.length > 0) {
|
|
5091
|
+
const isAllowed = uploadConfig.mimeTypes.some((pattern) => {
|
|
5092
|
+
if (pattern.endsWith('/*')) {
|
|
5093
|
+
const prefix = pattern.slice(0, -1);
|
|
5094
|
+
return file.type.startsWith(prefix);
|
|
5095
|
+
}
|
|
5096
|
+
return file.type === pattern;
|
|
5097
|
+
});
|
|
5098
|
+
if (!isAllowed) {
|
|
5099
|
+
this.uploadFileError.set(`File type "${file.type}" is not allowed`);
|
|
5100
|
+
this.pendingFile.set(null);
|
|
5101
|
+
return;
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
// Auto-populate metadata fields in form model
|
|
5106
|
+
const data = { ...this.formModel() };
|
|
5107
|
+
data['filename'] = file.name;
|
|
5108
|
+
data['mimeType'] = file.type;
|
|
5109
|
+
data['filesize'] = file.size;
|
|
5110
|
+
this.formModel.set(data);
|
|
5111
|
+
}
|
|
5112
|
+
/**
|
|
5113
|
+
* Handle file removed from the upload zone.
|
|
5114
|
+
*/
|
|
5115
|
+
onFileRemoved() {
|
|
5116
|
+
this.pendingFile.set(null);
|
|
5117
|
+
this.uploadFileError.set(null);
|
|
5118
|
+
// Clear auto-populated metadata
|
|
5119
|
+
const data = { ...this.formModel() };
|
|
5120
|
+
data['filename'] = '';
|
|
5121
|
+
data['mimeType'] = '';
|
|
5122
|
+
data['filesize'] = null;
|
|
5123
|
+
this.formModel.set(data);
|
|
5124
|
+
}
|
|
4357
5125
|
/**
|
|
4358
5126
|
* Handle form submission using Angular Signal Forms submit().
|
|
4359
5127
|
* submit() marks all fields as touched, then only calls the callback if valid.
|
|
5128
|
+
* For upload collections with a pending file, uses multipart upload via UploadService.
|
|
4360
5129
|
*/
|
|
4361
5130
|
async onSubmit() {
|
|
4362
5131
|
const ef = this.entityForm();
|
|
@@ -4369,7 +5138,7 @@ class EntityFormWidget {
|
|
|
4369
5138
|
this.formError.set(null);
|
|
4370
5139
|
try {
|
|
4371
5140
|
const slug = this.collection().slug;
|
|
4372
|
-
const data = this.formModel();
|
|
5141
|
+
const data = this.normalizeUploadFieldValues(this.formModel());
|
|
4373
5142
|
let result;
|
|
4374
5143
|
if (this.isGlobal()) {
|
|
4375
5144
|
// Global mode: always update (singleton)
|
|
@@ -4377,6 +5146,10 @@ class EntityFormWidget {
|
|
|
4377
5146
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4378
5147
|
result = await this.api.global(gSlug).update(data);
|
|
4379
5148
|
}
|
|
5149
|
+
else if (this.isUploadCol() && this.pendingFile()) {
|
|
5150
|
+
// Upload collection with a pending file: multipart upload
|
|
5151
|
+
result = await this.submitUploadCollection(slug, data);
|
|
5152
|
+
}
|
|
4380
5153
|
else if (this.mode() === 'create') {
|
|
4381
5154
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4382
5155
|
result = await this.api.collection(slug).create(data);
|
|
@@ -4390,6 +5163,7 @@ class EntityFormWidget {
|
|
|
4390
5163
|
}
|
|
4391
5164
|
this.originalData.set(result);
|
|
4392
5165
|
this.formModel.set({ ...result });
|
|
5166
|
+
this.pendingFile.set(null);
|
|
4393
5167
|
ef().reset();
|
|
4394
5168
|
this.saved.emit(result);
|
|
4395
5169
|
if (!this.suppressNavigation() && !this.isGlobal()) {
|
|
@@ -4405,6 +5179,7 @@ class EntityFormWidget {
|
|
|
4405
5179
|
}
|
|
4406
5180
|
finally {
|
|
4407
5181
|
this.isSubmitting.set(false);
|
|
5182
|
+
this.isUploadingFile.set(false);
|
|
4408
5183
|
}
|
|
4409
5184
|
});
|
|
4410
5185
|
// submit() didn't call the callback — form is invalid
|
|
@@ -4413,6 +5188,85 @@ class EntityFormWidget {
|
|
|
4413
5188
|
void this.liveAnnouncer.announce('Form submission failed. Please fix the errors above before submitting.', 'assertive');
|
|
4414
5189
|
}
|
|
4415
5190
|
}
|
|
5191
|
+
/**
|
|
5192
|
+
* For upload/relationship fields, the form may store a full document object
|
|
5193
|
+
* (e.g., after upload completes) but the DB expects just the UUID.
|
|
5194
|
+
* Extract the `id` property from any upload field values that are objects.
|
|
5195
|
+
*/
|
|
5196
|
+
normalizeUploadFieldValues(data) {
|
|
5197
|
+
const fields = this.collection().fields;
|
|
5198
|
+
const result = { ...data };
|
|
5199
|
+
for (const field of fields) {
|
|
5200
|
+
if (field.type === 'upload' && result[field.name] != null) {
|
|
5201
|
+
const val = result[field.name];
|
|
5202
|
+
if (typeof val === 'object' && val !== null) {
|
|
5203
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
5204
|
+
const obj = val; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
5205
|
+
if (typeof obj['id'] === 'string') {
|
|
5206
|
+
result[field.name] = obj['id'];
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
return result;
|
|
5212
|
+
}
|
|
5213
|
+
/**
|
|
5214
|
+
* Submit an upload collection form with file via multipart.
|
|
5215
|
+
* Converts non-file form fields to string key-value pairs for the FormData.
|
|
5216
|
+
*/
|
|
5217
|
+
submitUploadCollection(slug, data) {
|
|
5218
|
+
return new Promise((resolve, reject) => {
|
|
5219
|
+
const file = this.pendingFile();
|
|
5220
|
+
if (!file) {
|
|
5221
|
+
reject(new Error('No file selected'));
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
// Convert form data to string fields for FormData
|
|
5225
|
+
// Exclude auto-populated file metadata fields (they come from the server)
|
|
5226
|
+
const excludeFields = new Set([
|
|
5227
|
+
'filename', 'mimeType', 'filesize', 'path', 'url',
|
|
5228
|
+
'id', 'createdAt', 'updatedAt',
|
|
5229
|
+
]);
|
|
5230
|
+
const fields = {};
|
|
5231
|
+
for (const [key, value] of Object.entries(data)) {
|
|
5232
|
+
if (excludeFields.has(key))
|
|
5233
|
+
continue;
|
|
5234
|
+
if (value === null || value === undefined || value === '')
|
|
5235
|
+
continue;
|
|
5236
|
+
if (typeof value === 'object') {
|
|
5237
|
+
// Skip empty objects/arrays; serialize non-empty ones as JSON
|
|
5238
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
5239
|
+
const isEmptyObject = !Array.isArray(value) && Object.keys(value).length === 0;
|
|
5240
|
+
if ((Array.isArray(value) && value.length === 0) || isEmptyObject)
|
|
5241
|
+
continue;
|
|
5242
|
+
fields[key] = JSON.stringify(value);
|
|
5243
|
+
}
|
|
5244
|
+
else {
|
|
5245
|
+
fields[key] = String(value);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
this.isUploadingFile.set(true);
|
|
5249
|
+
this.uploadFileProgress.set(0);
|
|
5250
|
+
this.uploadService.uploadToCollection(slug, file, fields).subscribe({
|
|
5251
|
+
next: (progress) => {
|
|
5252
|
+
this.uploadFileProgress.set(progress.progress);
|
|
5253
|
+
if (progress.status === 'complete' && progress.result) {
|
|
5254
|
+
this.isUploadingFile.set(false);
|
|
5255
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
5256
|
+
resolve(progress.result);
|
|
5257
|
+
}
|
|
5258
|
+
else if (progress.status === 'error') {
|
|
5259
|
+
this.isUploadingFile.set(false);
|
|
5260
|
+
reject(new Error(progress.error ?? 'Upload failed'));
|
|
5261
|
+
}
|
|
5262
|
+
},
|
|
5263
|
+
error: (err) => {
|
|
5264
|
+
this.isUploadingFile.set(false);
|
|
5265
|
+
reject(err);
|
|
5266
|
+
},
|
|
5267
|
+
});
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
4416
5270
|
/**
|
|
4417
5271
|
* Handle cancel.
|
|
4418
5272
|
*/
|
|
@@ -4522,6 +5376,20 @@ class EntityFormWidget {
|
|
|
4522
5376
|
</mcms-alert>
|
|
4523
5377
|
}
|
|
4524
5378
|
|
|
5379
|
+
@if (isUploadCol()) {
|
|
5380
|
+
<mcms-collection-upload-zone
|
|
5381
|
+
[uploadConfig]="collection().upload"
|
|
5382
|
+
[pendingFile]="pendingFile()"
|
|
5383
|
+
[existingMedia]="mode() === 'edit' && !pendingFile() ? formModel() : null"
|
|
5384
|
+
[disabled]="mode() === 'view'"
|
|
5385
|
+
[isUploading]="isUploadingFile()"
|
|
5386
|
+
[uploadProgress]="uploadFileProgress()"
|
|
5387
|
+
[error]="uploadFileError()"
|
|
5388
|
+
(fileSelected)="onFileSelected($event)"
|
|
5389
|
+
(fileRemoved)="onFileRemoved()"
|
|
5390
|
+
/>
|
|
5391
|
+
}
|
|
5392
|
+
|
|
4525
5393
|
<div class="space-y-6">
|
|
4526
5394
|
@for (field of visibleFields(); track field.name) {
|
|
4527
5395
|
<mcms-field-renderer
|
|
@@ -4590,7 +5458,7 @@ class EntityFormWidget {
|
|
|
4590
5458
|
</div>
|
|
4591
5459
|
}
|
|
4592
5460
|
</div>
|
|
4593
|
-
`, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5461
|
+
`, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4594
5462
|
}
|
|
4595
5463
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EntityFormWidget, decorators: [{
|
|
4596
5464
|
type: Component,
|
|
@@ -4608,6 +5476,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4608
5476
|
BreadcrumbItem,
|
|
4609
5477
|
BreadcrumbSeparator,
|
|
4610
5478
|
VersionHistoryWidget,
|
|
5479
|
+
CollectionUploadZoneComponent,
|
|
4611
5480
|
],
|
|
4612
5481
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4613
5482
|
host: { class: 'block' },
|
|
@@ -4669,6 +5538,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4669
5538
|
</mcms-alert>
|
|
4670
5539
|
}
|
|
4671
5540
|
|
|
5541
|
+
@if (isUploadCol()) {
|
|
5542
|
+
<mcms-collection-upload-zone
|
|
5543
|
+
[uploadConfig]="collection().upload"
|
|
5544
|
+
[pendingFile]="pendingFile()"
|
|
5545
|
+
[existingMedia]="mode() === 'edit' && !pendingFile() ? formModel() : null"
|
|
5546
|
+
[disabled]="mode() === 'view'"
|
|
5547
|
+
[isUploading]="isUploadingFile()"
|
|
5548
|
+
[uploadProgress]="uploadFileProgress()"
|
|
5549
|
+
[error]="uploadFileError()"
|
|
5550
|
+
(fileSelected)="onFileSelected($event)"
|
|
5551
|
+
(fileRemoved)="onFileRemoved()"
|
|
5552
|
+
/>
|
|
5553
|
+
}
|
|
5554
|
+
|
|
4672
5555
|
<div class="space-y-6">
|
|
4673
5556
|
@for (field of visibleFields(); track field.name) {
|
|
4674
5557
|
<mcms-field-renderer
|
|
@@ -5077,15 +5960,33 @@ class EntityViewWidget {
|
|
|
5077
5960
|
const e = this.entity();
|
|
5078
5961
|
if (!e || !col.admin?.preview)
|
|
5079
5962
|
return null;
|
|
5080
|
-
|
|
5963
|
+
const preview = col.admin.preview;
|
|
5964
|
+
if (typeof preview === 'function') {
|
|
5081
5965
|
try {
|
|
5082
5966
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T extends Entity with index signature
|
|
5083
|
-
return
|
|
5967
|
+
return preview(e);
|
|
5084
5968
|
}
|
|
5085
5969
|
catch {
|
|
5086
5970
|
return null;
|
|
5087
5971
|
}
|
|
5088
5972
|
}
|
|
5973
|
+
// String template: interpolate {fieldName} placeholders with entity data
|
|
5974
|
+
if (typeof preview === 'string') {
|
|
5975
|
+
let hasEmpty = false;
|
|
5976
|
+
const url = preview.replace(/\{(\w+)\}/g, (_, field) => {
|
|
5977
|
+
const val = e[field];
|
|
5978
|
+
if (val == null || val === '') {
|
|
5979
|
+
hasEmpty = true;
|
|
5980
|
+
return '';
|
|
5981
|
+
}
|
|
5982
|
+
return String(val);
|
|
5983
|
+
});
|
|
5984
|
+
return hasEmpty ? null : url;
|
|
5985
|
+
}
|
|
5986
|
+
// Boolean true: use the server-rendered preview API endpoint
|
|
5987
|
+
if (preview === true) {
|
|
5988
|
+
return `/api/${col.slug}/${String(e.id)}/preview`;
|
|
5989
|
+
}
|
|
5089
5990
|
return null;
|
|
5090
5991
|
}, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
|
|
5091
5992
|
constructor() {
|
|
@@ -5317,44 +6218,64 @@ class EntityViewWidget {
|
|
|
5317
6218
|
this.loadEntity(this.collection().slug, this.entityId());
|
|
5318
6219
|
}
|
|
5319
6220
|
/**
|
|
5320
|
-
* Resolve relationship field values from IDs to display labels.
|
|
6221
|
+
* Resolve relationship and upload field values from IDs to display labels.
|
|
5321
6222
|
*/
|
|
5322
6223
|
resolveRelationships(entity) {
|
|
5323
6224
|
const fields = this.collection().fields;
|
|
5324
6225
|
const resolved = new Map();
|
|
5325
6226
|
const promises = [];
|
|
5326
6227
|
for (const field of fields) {
|
|
5327
|
-
if (field.type
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
6228
|
+
if (field.type === 'relationship') {
|
|
6229
|
+
const rawValue = entity[field.name];
|
|
6230
|
+
if (!rawValue || typeof rawValue !== 'string')
|
|
6231
|
+
continue;
|
|
6232
|
+
const config = field.collection();
|
|
6233
|
+
if (!isRecord(config) || typeof config['slug'] !== 'string')
|
|
6234
|
+
continue;
|
|
6235
|
+
const relSlug = config['slug'];
|
|
6236
|
+
const titleField = getTitleField(config);
|
|
6237
|
+
promises.push(this.api
|
|
6238
|
+
.collection(relSlug)
|
|
6239
|
+
.findById(rawValue)
|
|
6240
|
+
.then((doc) => {
|
|
6241
|
+
if (doc) {
|
|
6242
|
+
if (titleField !== 'id') {
|
|
6243
|
+
const titleValue = doc[titleField];
|
|
6244
|
+
if (typeof titleValue === 'string') {
|
|
6245
|
+
resolved.set(field.name, titleValue);
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
5347
6248
|
}
|
|
6249
|
+
resolved.set(field.name, String(doc['id'] ?? rawValue));
|
|
5348
6250
|
}
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
6251
|
+
else {
|
|
6252
|
+
resolved.set(field.name, 'Unknown');
|
|
6253
|
+
}
|
|
6254
|
+
})
|
|
6255
|
+
.catch(() => {
|
|
5352
6256
|
resolved.set(field.name, 'Unknown');
|
|
5353
|
-
}
|
|
5354
|
-
}
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
6257
|
+
}));
|
|
6258
|
+
}
|
|
6259
|
+
else if (field.type === 'upload') {
|
|
6260
|
+
const rawValue = entity[field.name];
|
|
6261
|
+
if (!rawValue || typeof rawValue !== 'string')
|
|
6262
|
+
continue;
|
|
6263
|
+
const relSlug = field.relationTo;
|
|
6264
|
+
promises.push(this.api
|
|
6265
|
+
.collection(relSlug)
|
|
6266
|
+
.findById(rawValue)
|
|
6267
|
+
.then((doc) => {
|
|
6268
|
+
if (doc && typeof doc['filename'] === 'string') {
|
|
6269
|
+
resolved.set(field.name, doc['filename']);
|
|
6270
|
+
}
|
|
6271
|
+
else {
|
|
6272
|
+
resolved.set(field.name, rawValue);
|
|
6273
|
+
}
|
|
6274
|
+
})
|
|
6275
|
+
.catch(() => {
|
|
6276
|
+
resolved.set(field.name, rawValue);
|
|
6277
|
+
}));
|
|
6278
|
+
}
|
|
5358
6279
|
}
|
|
5359
6280
|
if (promises.length > 0) {
|
|
5360
6281
|
Promise.all(promises).then(() => {
|
|
@@ -5880,6 +6801,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
5880
6801
|
}]
|
|
5881
6802
|
}] });
|
|
5882
6803
|
|
|
6804
|
+
const DEFAULT_GROUP = 'Collections';
|
|
6805
|
+
/**
|
|
6806
|
+
* Slugify a group name into a valid HTML id attribute value.
|
|
6807
|
+
* Lowercases, replaces non-alphanumeric runs with hyphens, trims leading/trailing hyphens.
|
|
6808
|
+
*/
|
|
6809
|
+
function slugify(name) {
|
|
6810
|
+
return name
|
|
6811
|
+
.toLowerCase()
|
|
6812
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
6813
|
+
.replace(/^-|-$/g, '');
|
|
6814
|
+
}
|
|
6815
|
+
/**
|
|
6816
|
+
* Group collections by their `admin.group` field.
|
|
6817
|
+
* Named groups appear first (in order of first appearance), the default "Collections" group last.
|
|
6818
|
+
* Each group includes a slugified `id` safe for use as an HTML id attribute.
|
|
6819
|
+
*/
|
|
6820
|
+
function groupCollections(collections) {
|
|
6821
|
+
const groupMap = new Map();
|
|
6822
|
+
for (const c of collections) {
|
|
6823
|
+
const name = c.admin?.group ?? DEFAULT_GROUP;
|
|
6824
|
+
const list = groupMap.get(name) ?? [];
|
|
6825
|
+
list.push(c);
|
|
6826
|
+
groupMap.set(name, list);
|
|
6827
|
+
}
|
|
6828
|
+
const groups = [];
|
|
6829
|
+
for (const [name, colls] of groupMap) {
|
|
6830
|
+
if (name !== DEFAULT_GROUP) {
|
|
6831
|
+
groups.push({ id: `group-${slugify(name)}`, name, collections: colls });
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
const defaultGroup = groupMap.get(DEFAULT_GROUP);
|
|
6835
|
+
if (defaultGroup) {
|
|
6836
|
+
groups.push({
|
|
6837
|
+
id: `group-${slugify(DEFAULT_GROUP)}`,
|
|
6838
|
+
name: DEFAULT_GROUP,
|
|
6839
|
+
collections: defaultGroup,
|
|
6840
|
+
});
|
|
6841
|
+
}
|
|
6842
|
+
return groups;
|
|
6843
|
+
}
|
|
6844
|
+
|
|
5883
6845
|
/**
|
|
5884
6846
|
* Admin Sidebar Widget
|
|
5885
6847
|
*
|
|
@@ -5925,27 +6887,7 @@ class AdminSidebarWidget {
|
|
|
5925
6887
|
/** Computed collections base path */
|
|
5926
6888
|
collectionsBasePath = computed(() => `${this.basePath()}/collections`, ...(ngDevMode ? [{ debugName: "collectionsBasePath" }] : []));
|
|
5927
6889
|
/** Collections grouped by admin.group field */
|
|
5928
|
-
collectionGroups = computed(() => {
|
|
5929
|
-
const collections = this.collections();
|
|
5930
|
-
const DEFAULT_GROUP = 'Collections';
|
|
5931
|
-
const groupMap = new Map();
|
|
5932
|
-
for (const c of collections) {
|
|
5933
|
-
const name = c.admin?.group ?? DEFAULT_GROUP;
|
|
5934
|
-
const list = groupMap.get(name) ?? [];
|
|
5935
|
-
list.push(c);
|
|
5936
|
-
groupMap.set(name, list);
|
|
5937
|
-
}
|
|
5938
|
-
// Named groups first (in order of first appearance), default last
|
|
5939
|
-
const groups = [];
|
|
5940
|
-
for (const [name, colls] of groupMap) {
|
|
5941
|
-
if (name !== DEFAULT_GROUP)
|
|
5942
|
-
groups.push({ name, collections: colls });
|
|
5943
|
-
}
|
|
5944
|
-
const defaultGroup = groupMap.get(DEFAULT_GROUP);
|
|
5945
|
-
if (defaultGroup)
|
|
5946
|
-
groups.push({ name: DEFAULT_GROUP, collections: defaultGroup });
|
|
5947
|
-
return groups;
|
|
5948
|
-
}, ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
|
|
6890
|
+
collectionGroups = computed(() => groupCollections(this.collections()), ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
|
|
5949
6891
|
/** Globals grouped by admin.group field */
|
|
5950
6892
|
globalGroups = computed(() => {
|
|
5951
6893
|
const globals = this.globals();
|
|
@@ -6099,7 +7041,7 @@ class AdminSidebarWidget {
|
|
|
6099
7041
|
/>
|
|
6100
7042
|
|
|
6101
7043
|
<!-- Collection Sections (grouped by admin.group) -->
|
|
6102
|
-
@for (group of collectionGroups(); track group.
|
|
7044
|
+
@for (group of collectionGroups(); track group.id) {
|
|
6103
7045
|
<mcms-sidebar-section [title]="group.name">
|
|
6104
7046
|
@for (collection of group.collections; track collection.slug) {
|
|
6105
7047
|
<mcms-sidebar-nav-item
|
|
@@ -6137,6 +7079,7 @@ class AdminSidebarWidget {
|
|
|
6137
7079
|
[label]="route.label"
|
|
6138
7080
|
[href]="basePath() + '/' + route.path"
|
|
6139
7081
|
[icon]="route.icon"
|
|
7082
|
+
[exact]="true"
|
|
6140
7083
|
/>
|
|
6141
7084
|
}
|
|
6142
7085
|
</mcms-sidebar-section>
|
|
@@ -6270,7 +7213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6270
7213
|
/>
|
|
6271
7214
|
|
|
6272
7215
|
<!-- Collection Sections (grouped by admin.group) -->
|
|
6273
|
-
@for (group of collectionGroups(); track group.
|
|
7216
|
+
@for (group of collectionGroups(); track group.id) {
|
|
6274
7217
|
<mcms-sidebar-section [title]="group.name">
|
|
6275
7218
|
@for (collection of group.collections; track collection.slug) {
|
|
6276
7219
|
<mcms-sidebar-nav-item
|
|
@@ -6308,6 +7251,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6308
7251
|
[label]="route.label"
|
|
6309
7252
|
[href]="basePath() + '/' + route.path"
|
|
6310
7253
|
[icon]="route.icon"
|
|
7254
|
+
[exact]="true"
|
|
6311
7255
|
/>
|
|
6312
7256
|
}
|
|
6313
7257
|
</mcms-sidebar-section>
|
|
@@ -7407,6 +8351,8 @@ class DashboardPage {
|
|
|
7407
8351
|
// Filter to only accessible, non-hidden collections
|
|
7408
8352
|
return all.filter((c) => !c.admin?.hidden && accessible.includes(c.slug));
|
|
7409
8353
|
}, ...(ngDevMode ? [{ debugName: "collections" }] : []));
|
|
8354
|
+
/** Visible collections grouped by admin.group. Named groups first, default last. */
|
|
8355
|
+
collectionGroups = computed(() => groupCollections(this.collections()), ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
|
|
7410
8356
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DashboardPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7411
8357
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: DashboardPage, isStandalone: true, selector: "mcms-dashboard", host: { classAttribute: "block max-w-6xl" }, ngImport: i0, template: `
|
|
7412
8358
|
<header class="mb-10">
|
|
@@ -7414,41 +8360,50 @@ class DashboardPage {
|
|
|
7414
8360
|
<p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
|
|
7415
8361
|
</header>
|
|
7416
8362
|
|
|
7417
|
-
|
|
7418
|
-
<
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
stroke-linecap="round"
|
|
7438
|
-
stroke-linejoin="round"
|
|
7439
|
-
stroke-width="1.5"
|
|
7440
|
-
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
7441
|
-
/>
|
|
7442
|
-
</svg>
|
|
7443
|
-
</div>
|
|
7444
|
-
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
7445
|
-
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
7446
|
-
Add collections to your configuration to start managing content.
|
|
7447
|
-
</p>
|
|
8363
|
+
@if (collectionGroups().length === 0) {
|
|
8364
|
+
<section aria-label="Collections">
|
|
8365
|
+
<div
|
|
8366
|
+
class="flex flex-col items-center justify-center p-16 bg-card/50 rounded-xl border border-dashed border-border/60"
|
|
8367
|
+
>
|
|
8368
|
+
<div class="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
8369
|
+
<svg
|
|
8370
|
+
aria-hidden="true"
|
|
8371
|
+
class="w-8 h-8 text-muted-foreground"
|
|
8372
|
+
fill="none"
|
|
8373
|
+
viewBox="0 0 24 24"
|
|
8374
|
+
stroke="currentColor"
|
|
8375
|
+
>
|
|
8376
|
+
<path
|
|
8377
|
+
stroke-linecap="round"
|
|
8378
|
+
stroke-linejoin="round"
|
|
8379
|
+
stroke-width="1.5"
|
|
8380
|
+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
8381
|
+
/>
|
|
8382
|
+
</svg>
|
|
7448
8383
|
</div>
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
8384
|
+
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
8385
|
+
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
8386
|
+
Add collections to your configuration to start managing content.
|
|
8387
|
+
</p>
|
|
8388
|
+
</div>
|
|
8389
|
+
</section>
|
|
8390
|
+
} @else {
|
|
8391
|
+
@for (group of collectionGroups(); track group.id) {
|
|
8392
|
+
<section class="mb-10" [attr.aria-labelledby]="group.id">
|
|
8393
|
+
<h2
|
|
8394
|
+
[id]="group.id"
|
|
8395
|
+
class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"
|
|
8396
|
+
>
|
|
8397
|
+
{{ group.name }}
|
|
8398
|
+
</h2>
|
|
8399
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
8400
|
+
@for (collection of group.collections; track collection.slug) {
|
|
8401
|
+
<mcms-collection-card [collection]="collection" [basePath]="basePath" />
|
|
8402
|
+
}
|
|
8403
|
+
</div>
|
|
8404
|
+
</section>
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
7452
8407
|
`, isInline: true, dependencies: [{ kind: "component", type: CollectionCardWidget, selector: "mcms-collection-card", inputs: ["collection", "basePath", "showDocumentCount"], outputs: ["viewAll"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7453
8408
|
}
|
|
7454
8409
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DashboardPage, decorators: [{
|
|
@@ -7464,41 +8419,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
7464
8419
|
<p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
|
|
7465
8420
|
</header>
|
|
7466
8421
|
|
|
7467
|
-
|
|
7468
|
-
<
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
8422
|
+
@if (collectionGroups().length === 0) {
|
|
8423
|
+
<section aria-label="Collections">
|
|
8424
|
+
<div
|
|
8425
|
+
class="flex flex-col items-center justify-center p-16 bg-card/50 rounded-xl border border-dashed border-border/60"
|
|
8426
|
+
>
|
|
8427
|
+
<div class="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
8428
|
+
<svg
|
|
8429
|
+
aria-hidden="true"
|
|
8430
|
+
class="w-8 h-8 text-muted-foreground"
|
|
8431
|
+
fill="none"
|
|
8432
|
+
viewBox="0 0 24 24"
|
|
8433
|
+
stroke="currentColor"
|
|
8434
|
+
>
|
|
8435
|
+
<path
|
|
8436
|
+
stroke-linecap="round"
|
|
8437
|
+
stroke-linejoin="round"
|
|
8438
|
+
stroke-width="1.5"
|
|
8439
|
+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
8440
|
+
/>
|
|
8441
|
+
</svg>
|
|
8442
|
+
</div>
|
|
8443
|
+
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
8444
|
+
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
8445
|
+
Add collections to your configuration to start managing content.
|
|
8446
|
+
</p>
|
|
8447
|
+
</div>
|
|
8448
|
+
</section>
|
|
8449
|
+
} @else {
|
|
8450
|
+
@for (group of collectionGroups(); track group.id) {
|
|
8451
|
+
<section class="mb-10" [attr.aria-labelledby]="group.id">
|
|
8452
|
+
<h2
|
|
8453
|
+
[id]="group.id"
|
|
8454
|
+
class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"
|
|
7477
8455
|
>
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
stroke="currentColor"
|
|
7485
|
-
>
|
|
7486
|
-
<path
|
|
7487
|
-
stroke-linecap="round"
|
|
7488
|
-
stroke-linejoin="round"
|
|
7489
|
-
stroke-width="1.5"
|
|
7490
|
-
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
7491
|
-
/>
|
|
7492
|
-
</svg>
|
|
7493
|
-
</div>
|
|
7494
|
-
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
7495
|
-
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
7496
|
-
Add collections to your configuration to start managing content.
|
|
7497
|
-
</p>
|
|
8456
|
+
{{ group.name }}
|
|
8457
|
+
</h2>
|
|
8458
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
8459
|
+
@for (collection of group.collections; track collection.slug) {
|
|
8460
|
+
<mcms-collection-card [collection]="collection" [basePath]="basePath" />
|
|
8461
|
+
}
|
|
7498
8462
|
</div>
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
8463
|
+
</section>
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
7502
8466
|
`,
|
|
7503
8467
|
}]
|
|
7504
8468
|
}] });
|
|
@@ -8854,15 +9818,109 @@ var collectionList_page = /*#__PURE__*/Object.freeze({
|
|
|
8854
9818
|
CollectionListPage: CollectionListPage
|
|
8855
9819
|
});
|
|
8856
9820
|
|
|
9821
|
+
/**
|
|
9822
|
+
* Collection View Page Component
|
|
9823
|
+
*
|
|
9824
|
+
* Displays a read-only view of a document using the EntityViewWidget.
|
|
9825
|
+
* Preview is available via the "Open Page" link in the entity header
|
|
9826
|
+
* (opens in a new tab). Live preview iframe is only on the edit page,
|
|
9827
|
+
* since the view page is read-only and has no changes to preview live.
|
|
9828
|
+
*/
|
|
9829
|
+
class CollectionViewPage {
|
|
9830
|
+
route = inject(ActivatedRoute);
|
|
9831
|
+
router = inject(Router);
|
|
9832
|
+
basePath = '/admin/collections';
|
|
9833
|
+
// Reactive slug signal that updates when route params change
|
|
9834
|
+
slug = toSignal(this.route.paramMap.pipe(map((params) => params.get('slug') ?? '')), { initialValue: this.route.snapshot.paramMap.get('slug') ?? '' });
|
|
9835
|
+
// Reactive entity ID signal
|
|
9836
|
+
entityId = toSignal(this.route.paramMap.pipe(map((params) => params.get('id') ?? '')), {
|
|
9837
|
+
initialValue: this.route.snapshot.paramMap.get('id') ?? '',
|
|
9838
|
+
});
|
|
9839
|
+
collection = computed(() => {
|
|
9840
|
+
const currentSlug = this.slug();
|
|
9841
|
+
if (!currentSlug)
|
|
9842
|
+
return undefined;
|
|
9843
|
+
const collections = getCollectionsFromRouteData(this.route.parent?.snapshot.data);
|
|
9844
|
+
return collections.find((c) => c.slug === currentSlug);
|
|
9845
|
+
}, ...(ngDevMode ? [{ debugName: "collection" }] : []));
|
|
9846
|
+
onEdit(entity) {
|
|
9847
|
+
const col = this.collection();
|
|
9848
|
+
if (col) {
|
|
9849
|
+
this.router.navigate([this.basePath, col.slug, entity.id, 'edit']);
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
onDelete(_entity) {
|
|
9853
|
+
// Navigation is handled by EntityViewWidget
|
|
9854
|
+
}
|
|
9855
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9856
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: CollectionViewPage, isStandalone: true, selector: "mcms-collection-view", host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
9857
|
+
@if (collection(); as col) {
|
|
9858
|
+
@if (entityId(); as id) {
|
|
9859
|
+
<mcms-entity-view
|
|
9860
|
+
[collection]="col"
|
|
9861
|
+
[entityId]="id"
|
|
9862
|
+
[basePath]="basePath"
|
|
9863
|
+
(edit)="onEdit($event)"
|
|
9864
|
+
(delete_)="onDelete($event)"
|
|
9865
|
+
/>
|
|
9866
|
+
} @else {
|
|
9867
|
+
<div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
|
|
9868
|
+
}
|
|
9869
|
+
} @else {
|
|
9870
|
+
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9871
|
+
}
|
|
9872
|
+
`, 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 });
|
|
9873
|
+
}
|
|
9874
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, decorators: [{
|
|
9875
|
+
type: Component,
|
|
9876
|
+
args: [{
|
|
9877
|
+
selector: 'mcms-collection-view',
|
|
9878
|
+
imports: [EntityViewWidget],
|
|
9879
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9880
|
+
host: { class: 'block' },
|
|
9881
|
+
template: `
|
|
9882
|
+
@if (collection(); as col) {
|
|
9883
|
+
@if (entityId(); as id) {
|
|
9884
|
+
<mcms-entity-view
|
|
9885
|
+
[collection]="col"
|
|
9886
|
+
[entityId]="id"
|
|
9887
|
+
[basePath]="basePath"
|
|
9888
|
+
(edit)="onEdit($event)"
|
|
9889
|
+
(delete_)="onDelete($event)"
|
|
9890
|
+
/>
|
|
9891
|
+
} @else {
|
|
9892
|
+
<div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
|
|
9893
|
+
}
|
|
9894
|
+
} @else {
|
|
9895
|
+
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9896
|
+
}
|
|
9897
|
+
`,
|
|
9898
|
+
}]
|
|
9899
|
+
}] });
|
|
9900
|
+
|
|
9901
|
+
var collectionView_page = /*#__PURE__*/Object.freeze({
|
|
9902
|
+
__proto__: null,
|
|
9903
|
+
CollectionViewPage: CollectionViewPage
|
|
9904
|
+
});
|
|
9905
|
+
|
|
8857
9906
|
/**
|
|
8858
9907
|
* Live Preview Widget
|
|
8859
9908
|
*
|
|
8860
9909
|
* Displays an iframe that shows a live preview of the document being edited.
|
|
8861
|
-
*
|
|
9910
|
+
*
|
|
9911
|
+
* Two modes based on preview config type:
|
|
9912
|
+
* - `preview: true` (server-rendered HTML): iframe loads API endpoint with scripts enabled
|
|
9913
|
+
* for postMessage live updates.
|
|
9914
|
+
* - `preview: string/function` (URL-based): iframe loads the page URL with scripts DISABLED.
|
|
9915
|
+
* This prevents loading a second Angular app instance (with Vite HMR, SSR hydration, etc.)
|
|
9916
|
+
* which causes tab crashes in dev mode. The SSR-rendered HTML displays correctly without JS.
|
|
9917
|
+
* Use the Refresh button to see form changes reflected in the preview.
|
|
9918
|
+
*
|
|
9919
|
+
* The iframe is declared statically in the template (no dynamic bindings) to avoid NG0910.
|
|
9920
|
+
* Its src/sandbox attributes are set via nativeElement in an effect().
|
|
8862
9921
|
*/
|
|
8863
9922
|
class LivePreviewComponent {
|
|
8864
9923
|
document = inject(DOCUMENT);
|
|
8865
|
-
sanitizer = inject(DomSanitizer);
|
|
8866
9924
|
destroyRef = inject(DestroyRef);
|
|
8867
9925
|
/** Preview configuration from collection admin config */
|
|
8868
9926
|
preview = input.required(...(ngDevMode ? [{ debugName: "preview" }] : []));
|
|
@@ -8878,8 +9936,8 @@ class LivePreviewComponent {
|
|
|
8878
9936
|
deviceSize = signal('desktop', ...(ngDevMode ? [{ debugName: "deviceSize" }] : []));
|
|
8879
9937
|
/** Refresh counter to force iframe reload */
|
|
8880
9938
|
refreshCounter = signal(0, ...(ngDevMode ? [{ debugName: "refreshCounter" }] : []));
|
|
8881
|
-
/** Reference to the iframe element */
|
|
8882
|
-
|
|
9939
|
+
/** Reference to the static iframe element (available when previewUrl is non-null) */
|
|
9940
|
+
previewIframe = viewChild('previewIframe', ...(ngDevMode ? [{ debugName: "previewIframe" }] : []));
|
|
8883
9941
|
/** Compute the raw preview URL */
|
|
8884
9942
|
previewUrl = computed(() => {
|
|
8885
9943
|
// Force recomputation on refresh
|
|
@@ -8896,18 +9954,25 @@ class LivePreviewComponent {
|
|
|
8896
9954
|
return null;
|
|
8897
9955
|
}
|
|
8898
9956
|
}
|
|
9957
|
+
// URL template string: interpolate {fieldName} placeholders with form data
|
|
9958
|
+
// Return null if any placeholder resolves to empty (data not yet loaded)
|
|
9959
|
+
if (typeof previewConfig === 'string') {
|
|
9960
|
+
let hasEmptyField = false;
|
|
9961
|
+
const url = previewConfig.replace(/\{(\w+)\}/g, (_, field) => {
|
|
9962
|
+
const val = data[field];
|
|
9963
|
+
if (val == null || val === '') {
|
|
9964
|
+
hasEmptyField = true;
|
|
9965
|
+
return '';
|
|
9966
|
+
}
|
|
9967
|
+
return String(val);
|
|
9968
|
+
});
|
|
9969
|
+
return hasEmptyField ? null : url;
|
|
9970
|
+
}
|
|
8899
9971
|
if (previewConfig === true && id) {
|
|
8900
9972
|
return `/api/${slug}/${id}/preview`;
|
|
8901
9973
|
}
|
|
8902
9974
|
return null;
|
|
8903
9975
|
}, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
|
|
8904
|
-
/** Sanitized preview URL for iframe binding */
|
|
8905
|
-
safePreviewUrl = computed(() => {
|
|
8906
|
-
const url = this.previewUrl();
|
|
8907
|
-
if (!url)
|
|
8908
|
-
return null;
|
|
8909
|
-
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
|
8910
|
-
}, ...(ngDevMode ? [{ debugName: "safePreviewUrl" }] : []));
|
|
8911
9976
|
/** Computed iframe width based on device size */
|
|
8912
9977
|
iframeWidth = computed(() => {
|
|
8913
9978
|
switch (this.deviceSize()) {
|
|
@@ -8919,21 +9984,57 @@ class LivePreviewComponent {
|
|
|
8919
9984
|
return '100%';
|
|
8920
9985
|
}
|
|
8921
9986
|
}, ...(ngDevMode ? [{ debugName: "iframeWidth" }] : []));
|
|
9987
|
+
/** Sandbox attribute value based on preview mode */
|
|
9988
|
+
sandboxValue = computed(() => {
|
|
9989
|
+
const previewConfig = this.preview();
|
|
9990
|
+
if (previewConfig === true) {
|
|
9991
|
+
// Server-rendered HTML: scripts needed for postMessage live updates
|
|
9992
|
+
return 'allow-same-origin allow-scripts allow-popups allow-forms';
|
|
9993
|
+
}
|
|
9994
|
+
// URL-based preview: no scripts to prevent full Angular app from loading
|
|
9995
|
+
return 'allow-same-origin allow-popups allow-forms';
|
|
9996
|
+
}, ...(ngDevMode ? [{ debugName: "sandboxValue" }] : []));
|
|
8922
9997
|
/** Debounce timer for postMessage updates */
|
|
8923
9998
|
debounceTimer = undefined;
|
|
8924
9999
|
constructor() {
|
|
8925
|
-
//
|
|
10000
|
+
// Effect 1: Set iframe src and sandbox when URL or sandbox config changes.
|
|
10001
|
+
// Uses untracked() for iframeWidth so device size toggles don't trigger a reload.
|
|
10002
|
+
effect(() => {
|
|
10003
|
+
const iframeRef = this.previewIframe();
|
|
10004
|
+
if (!iframeRef)
|
|
10005
|
+
return;
|
|
10006
|
+
const iframe = iframeRef.nativeElement;
|
|
10007
|
+
const url = this.previewUrl();
|
|
10008
|
+
if (!url)
|
|
10009
|
+
return;
|
|
10010
|
+
iframe.setAttribute('sandbox', this.sandboxValue());
|
|
10011
|
+
iframe.src = url;
|
|
10012
|
+
// Set initial width without tracking the signal
|
|
10013
|
+
iframe.style.width = untracked(() => this.iframeWidth());
|
|
10014
|
+
});
|
|
10015
|
+
// Effect 2: Update iframe width only (no reload).
|
|
10016
|
+
// Changing CSS width on an iframe does not trigger navigation.
|
|
10017
|
+
effect(() => {
|
|
10018
|
+
const iframeRef = this.previewIframe();
|
|
10019
|
+
if (!iframeRef)
|
|
10020
|
+
return;
|
|
10021
|
+
iframeRef.nativeElement.style.width = this.iframeWidth();
|
|
10022
|
+
});
|
|
10023
|
+
// Send form data to iframe via postMessage whenever data changes.
|
|
10024
|
+
// Only effective for server-rendered previews (preview: true) where
|
|
10025
|
+
// allow-scripts is enabled. URL-based previews have scripts disabled
|
|
10026
|
+
// so the postMessage is a no-op (which is fine).
|
|
8926
10027
|
effect(() => {
|
|
8927
10028
|
const data = this.documentData();
|
|
8928
|
-
const
|
|
8929
|
-
if (!
|
|
10029
|
+
const iframeRef = this.previewIframe();
|
|
10030
|
+
if (!iframeRef?.nativeElement.contentWindow)
|
|
8930
10031
|
return;
|
|
8931
10032
|
// Debounce to avoid thrashing
|
|
8932
10033
|
if (this.debounceTimer) {
|
|
8933
10034
|
clearTimeout(this.debounceTimer);
|
|
8934
10035
|
}
|
|
8935
10036
|
this.debounceTimer = this.document.defaultView?.setTimeout(() => {
|
|
8936
|
-
const iframeWindow =
|
|
10037
|
+
const iframeWindow = iframeRef.nativeElement.contentWindow;
|
|
8937
10038
|
if (iframeWindow) {
|
|
8938
10039
|
const targetOrigin = this.document.defaultView?.location?.origin ?? '';
|
|
8939
10040
|
iframeWindow.postMessage({ type: 'momentum-preview-update', data }, targetOrigin);
|
|
@@ -8969,7 +10070,7 @@ class LivePreviewComponent {
|
|
|
8969
10070
|
this.refreshCounter.update((c) => c + 1);
|
|
8970
10071
|
}
|
|
8971
10072
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: LivePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8972
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: LivePreviewComponent, isStandalone: true, selector: "mcms-live-preview", inputs: { preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: true, transformFunction: null }, documentData: { classPropertyName: "documentData", publicName: "documentData", isSignal: true, isRequired: true, transformFunction: null }, collectionSlug: { classPropertyName: "collectionSlug", publicName: "collectionSlug", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editBlockRequest: "editBlockRequest" }, host: { classAttribute: "flex flex-col h-full border-l border-border" }, viewQueries: [{ propertyName: "
|
|
10073
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: LivePreviewComponent, isStandalone: true, selector: "mcms-live-preview", inputs: { preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: true, transformFunction: null }, documentData: { classPropertyName: "documentData", publicName: "documentData", isSignal: true, isRequired: true, transformFunction: null }, collectionSlug: { classPropertyName: "collectionSlug", publicName: "collectionSlug", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editBlockRequest: "editBlockRequest" }, host: { classAttribute: "flex flex-col h-full border-l border-border" }, viewQueries: [{ propertyName: "previewIframe", first: true, predicate: ["previewIframe"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
8973
10074
|
<!-- Preview toolbar -->
|
|
8974
10075
|
<div class="flex items-center gap-2 px-4 py-2 border-b border-border bg-muted/50">
|
|
8975
10076
|
<span class="text-sm font-medium text-foreground">Preview</span>
|
|
@@ -9037,15 +10138,14 @@ class LivePreviewComponent {
|
|
|
9037
10138
|
|
|
9038
10139
|
<!-- Preview iframe container -->
|
|
9039
10140
|
<div class="flex-1 overflow-auto bg-muted/30 flex justify-center p-4">
|
|
9040
|
-
@if (
|
|
10141
|
+
@if (previewUrl()) {
|
|
10142
|
+
<!-- Static iframe with no dynamic bindings (avoids NG0910).
|
|
10143
|
+
src/sandbox/width are set via nativeElement in an effect(). -->
|
|
9041
10144
|
<iframe
|
|
9042
|
-
#
|
|
9043
|
-
[src]="url"
|
|
9044
|
-
[style.width]="iframeWidth()"
|
|
9045
|
-
class="h-full bg-white border border-border rounded-md shadow-sm transition-[width] duration-300"
|
|
9046
|
-
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
10145
|
+
#previewIframe
|
|
9047
10146
|
title="Live document preview"
|
|
9048
10147
|
data-testid="preview-iframe"
|
|
10148
|
+
class="h-full bg-white border border-border rounded-md shadow-sm transition-[width] duration-300"
|
|
9049
10149
|
></iframe>
|
|
9050
10150
|
} @else {
|
|
9051
10151
|
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
@@ -9123,255 +10223,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
9123
10223
|
(click)="refreshPreview()"
|
|
9124
10224
|
data-testid="preview-refresh"
|
|
9125
10225
|
aria-label="Refresh preview"
|
|
9126
|
-
>
|
|
9127
|
-
↻ Refresh
|
|
9128
|
-
</button>
|
|
9129
|
-
</div>
|
|
9130
|
-
|
|
9131
|
-
<!-- Preview iframe container -->
|
|
9132
|
-
<div class="flex-1 overflow-auto bg-muted/30 flex justify-center p-4">
|
|
9133
|
-
@if (
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
></iframe>
|
|
9143
|
-
} @else {
|
|
9144
|
-
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
9145
|
-
Preview not available
|
|
9146
|
-
</div>
|
|
9147
|
-
}
|
|
9148
|
-
</div>
|
|
9149
|
-
`,
|
|
9150
|
-
}]
|
|
9151
|
-
}], ctorParameters: () => [], propDecorators: { preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: true }] }], documentData: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentData", required: true }] }], collectionSlug: [{ type: i0.Input, args: [{ isSignal: true, alias: "collectionSlug", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], editBlockRequest: [{ type: i0.Output, args: ["editBlockRequest"] }], previewFrame: [{ type: i0.ViewChild, args: ['previewFrame', { isSignal: true }] }] } });
|
|
9152
|
-
|
|
9153
|
-
/**
|
|
9154
|
-
* Collection View Page Component
|
|
9155
|
-
*
|
|
9156
|
-
* Displays a read-only view of a document using the EntityViewWidget.
|
|
9157
|
-
* When preview is enabled, shows a toggleable live preview panel.
|
|
9158
|
-
*/
|
|
9159
|
-
class CollectionViewPage {
|
|
9160
|
-
route = inject(ActivatedRoute);
|
|
9161
|
-
router = inject(Router);
|
|
9162
|
-
basePath = '/admin/collections';
|
|
9163
|
-
/** Whether the live preview panel is visible */
|
|
9164
|
-
showPreview = signal(true, ...(ngDevMode ? [{ debugName: "showPreview" }] : []));
|
|
9165
|
-
/** Reference to the entity view widget to read its entity data */
|
|
9166
|
-
entityViewRef = viewChild('entityView', ...(ngDevMode ? [{ debugName: "entityViewRef" }] : []));
|
|
9167
|
-
// Reactive slug signal that updates when route params change
|
|
9168
|
-
slug = toSignal(this.route.paramMap.pipe(map((params) => params.get('slug') ?? '')), { initialValue: this.route.snapshot.paramMap.get('slug') ?? '' });
|
|
9169
|
-
// Reactive entity ID signal
|
|
9170
|
-
entityId = toSignal(this.route.paramMap.pipe(map((params) => params.get('id') ?? '')), {
|
|
9171
|
-
initialValue: this.route.snapshot.paramMap.get('id') ?? '',
|
|
9172
|
-
});
|
|
9173
|
-
collection = computed(() => {
|
|
9174
|
-
const currentSlug = this.slug();
|
|
9175
|
-
if (!currentSlug)
|
|
9176
|
-
return undefined;
|
|
9177
|
-
const collections = getCollectionsFromRouteData(this.route.parent?.snapshot.data);
|
|
9178
|
-
return collections.find((c) => c.slug === currentSlug);
|
|
9179
|
-
}, ...(ngDevMode ? [{ debugName: "collection" }] : []));
|
|
9180
|
-
/** Preview config from collection admin settings */
|
|
9181
|
-
previewConfig = computed(() => {
|
|
9182
|
-
const col = this.collection();
|
|
9183
|
-
return col?.admin?.preview || undefined;
|
|
9184
|
-
}, ...(ngDevMode ? [{ debugName: "previewConfig" }] : []));
|
|
9185
|
-
/** Entity data from the entity view widget (for live preview) */
|
|
9186
|
-
viewEntityData = computed(() => {
|
|
9187
|
-
const view = this.entityViewRef();
|
|
9188
|
-
const entity = view?.entity();
|
|
9189
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T extends Entity with index signature
|
|
9190
|
-
return entity ?? {};
|
|
9191
|
-
}, ...(ngDevMode ? [{ debugName: "viewEntityData" }] : []));
|
|
9192
|
-
onEdit(entity) {
|
|
9193
|
-
const col = this.collection();
|
|
9194
|
-
if (col) {
|
|
9195
|
-
this.router.navigate([this.basePath, col.slug, entity.id, 'edit']);
|
|
9196
|
-
}
|
|
9197
|
-
}
|
|
9198
|
-
onDelete(_entity) {
|
|
9199
|
-
// Navigation is handled by EntityViewWidget
|
|
9200
|
-
}
|
|
9201
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9202
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: CollectionViewPage, isStandalone: true, selector: "mcms-collection-view", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "entityViewRef", first: true, predicate: ["entityView"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
9203
|
-
@if (collection(); as col) {
|
|
9204
|
-
@if (entityId(); as id) {
|
|
9205
|
-
@if (previewConfig(); as preview) {
|
|
9206
|
-
@if (showPreview()) {
|
|
9207
|
-
<!-- Split layout: entity view + preview -->
|
|
9208
|
-
<div class="flex gap-0 h-[calc(100vh-64px)]" data-testid="preview-layout">
|
|
9209
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
9210
|
-
<mcms-entity-view
|
|
9211
|
-
#entityView
|
|
9212
|
-
[collection]="col"
|
|
9213
|
-
[entityId]="id"
|
|
9214
|
-
[basePath]="basePath"
|
|
9215
|
-
(edit)="onEdit($event)"
|
|
9216
|
-
(delete_)="onDelete($event)"
|
|
9217
|
-
>
|
|
9218
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9219
|
-
<button
|
|
9220
|
-
mcms-button
|
|
9221
|
-
variant="ghost"
|
|
9222
|
-
size="sm"
|
|
9223
|
-
data-testid="preview-toggle"
|
|
9224
|
-
(click)="showPreview.set(false)"
|
|
9225
|
-
>
|
|
9226
|
-
Hide Preview
|
|
9227
|
-
</button>
|
|
9228
|
-
</div>
|
|
9229
|
-
</mcms-entity-view>
|
|
9230
|
-
</div>
|
|
9231
|
-
<div class="w-[50%] min-w-[400px] max-w-[720px]">
|
|
9232
|
-
<mcms-live-preview
|
|
9233
|
-
[preview]="preview"
|
|
9234
|
-
[documentData]="viewEntityData()"
|
|
9235
|
-
[collectionSlug]="col.slug"
|
|
9236
|
-
[entityId]="id"
|
|
9237
|
-
/>
|
|
9238
|
-
</div>
|
|
9239
|
-
</div>
|
|
9240
|
-
} @else {
|
|
9241
|
-
<!-- Full-width view (preview hidden) -->
|
|
9242
|
-
<mcms-entity-view
|
|
9243
|
-
#entityView
|
|
9244
|
-
[collection]="col"
|
|
9245
|
-
[entityId]="id"
|
|
9246
|
-
[basePath]="basePath"
|
|
9247
|
-
(edit)="onEdit($event)"
|
|
9248
|
-
(delete_)="onDelete($event)"
|
|
9249
|
-
>
|
|
9250
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9251
|
-
<button
|
|
9252
|
-
mcms-button
|
|
9253
|
-
variant="ghost"
|
|
9254
|
-
size="sm"
|
|
9255
|
-
data-testid="preview-toggle"
|
|
9256
|
-
(click)="showPreview.set(true)"
|
|
9257
|
-
>
|
|
9258
|
-
Show Preview
|
|
9259
|
-
</button>
|
|
9260
|
-
</div>
|
|
9261
|
-
</mcms-entity-view>
|
|
9262
|
-
}
|
|
9263
|
-
} @else {
|
|
9264
|
-
<!-- No preview configured -->
|
|
9265
|
-
<mcms-entity-view
|
|
9266
|
-
#entityView
|
|
9267
|
-
[collection]="col"
|
|
9268
|
-
[entityId]="id"
|
|
9269
|
-
[basePath]="basePath"
|
|
9270
|
-
(edit)="onEdit($event)"
|
|
9271
|
-
(delete_)="onDelete($event)"
|
|
9272
|
-
/>
|
|
9273
|
-
}
|
|
9274
|
-
} @else {
|
|
9275
|
-
<div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
|
|
9276
|
-
}
|
|
9277
|
-
} @else {
|
|
9278
|
-
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9279
|
-
}
|
|
9280
|
-
`, 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: 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 });
|
|
9281
|
-
}
|
|
9282
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, decorators: [{
|
|
9283
|
-
type: Component,
|
|
9284
|
-
args: [{
|
|
9285
|
-
selector: 'mcms-collection-view',
|
|
9286
|
-
imports: [EntityViewWidget, LivePreviewComponent, Button],
|
|
9287
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9288
|
-
host: { class: 'block' },
|
|
9289
|
-
template: `
|
|
9290
|
-
@if (collection(); as col) {
|
|
9291
|
-
@if (entityId(); as id) {
|
|
9292
|
-
@if (previewConfig(); as preview) {
|
|
9293
|
-
@if (showPreview()) {
|
|
9294
|
-
<!-- Split layout: entity view + preview -->
|
|
9295
|
-
<div class="flex gap-0 h-[calc(100vh-64px)]" data-testid="preview-layout">
|
|
9296
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
9297
|
-
<mcms-entity-view
|
|
9298
|
-
#entityView
|
|
9299
|
-
[collection]="col"
|
|
9300
|
-
[entityId]="id"
|
|
9301
|
-
[basePath]="basePath"
|
|
9302
|
-
(edit)="onEdit($event)"
|
|
9303
|
-
(delete_)="onDelete($event)"
|
|
9304
|
-
>
|
|
9305
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9306
|
-
<button
|
|
9307
|
-
mcms-button
|
|
9308
|
-
variant="ghost"
|
|
9309
|
-
size="sm"
|
|
9310
|
-
data-testid="preview-toggle"
|
|
9311
|
-
(click)="showPreview.set(false)"
|
|
9312
|
-
>
|
|
9313
|
-
Hide Preview
|
|
9314
|
-
</button>
|
|
9315
|
-
</div>
|
|
9316
|
-
</mcms-entity-view>
|
|
9317
|
-
</div>
|
|
9318
|
-
<div class="w-[50%] min-w-[400px] max-w-[720px]">
|
|
9319
|
-
<mcms-live-preview
|
|
9320
|
-
[preview]="preview"
|
|
9321
|
-
[documentData]="viewEntityData()"
|
|
9322
|
-
[collectionSlug]="col.slug"
|
|
9323
|
-
[entityId]="id"
|
|
9324
|
-
/>
|
|
9325
|
-
</div>
|
|
9326
|
-
</div>
|
|
9327
|
-
} @else {
|
|
9328
|
-
<!-- Full-width view (preview hidden) -->
|
|
9329
|
-
<mcms-entity-view
|
|
9330
|
-
#entityView
|
|
9331
|
-
[collection]="col"
|
|
9332
|
-
[entityId]="id"
|
|
9333
|
-
[basePath]="basePath"
|
|
9334
|
-
(edit)="onEdit($event)"
|
|
9335
|
-
(delete_)="onDelete($event)"
|
|
9336
|
-
>
|
|
9337
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9338
|
-
<button
|
|
9339
|
-
mcms-button
|
|
9340
|
-
variant="ghost"
|
|
9341
|
-
size="sm"
|
|
9342
|
-
data-testid="preview-toggle"
|
|
9343
|
-
(click)="showPreview.set(true)"
|
|
9344
|
-
>
|
|
9345
|
-
Show Preview
|
|
9346
|
-
</button>
|
|
9347
|
-
</div>
|
|
9348
|
-
</mcms-entity-view>
|
|
9349
|
-
}
|
|
9350
|
-
} @else {
|
|
9351
|
-
<!-- No preview configured -->
|
|
9352
|
-
<mcms-entity-view
|
|
9353
|
-
#entityView
|
|
9354
|
-
[collection]="col"
|
|
9355
|
-
[entityId]="id"
|
|
9356
|
-
[basePath]="basePath"
|
|
9357
|
-
(edit)="onEdit($event)"
|
|
9358
|
-
(delete_)="onDelete($event)"
|
|
9359
|
-
/>
|
|
9360
|
-
}
|
|
10226
|
+
>
|
|
10227
|
+
↻ Refresh
|
|
10228
|
+
</button>
|
|
10229
|
+
</div>
|
|
10230
|
+
|
|
10231
|
+
<!-- Preview iframe container -->
|
|
10232
|
+
<div class="flex-1 overflow-auto bg-muted/30 flex justify-center p-4">
|
|
10233
|
+
@if (previewUrl()) {
|
|
10234
|
+
<!-- Static iframe with no dynamic bindings (avoids NG0910).
|
|
10235
|
+
src/sandbox/width are set via nativeElement in an effect(). -->
|
|
10236
|
+
<iframe
|
|
10237
|
+
#previewIframe
|
|
10238
|
+
title="Live document preview"
|
|
10239
|
+
data-testid="preview-iframe"
|
|
10240
|
+
class="h-full bg-white border border-border rounded-md shadow-sm transition-[width] duration-300"
|
|
10241
|
+
></iframe>
|
|
9361
10242
|
} @else {
|
|
9362
|
-
<div class="
|
|
10243
|
+
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
10244
|
+
Preview not available
|
|
10245
|
+
</div>
|
|
9363
10246
|
}
|
|
9364
|
-
|
|
9365
|
-
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9366
|
-
}
|
|
10247
|
+
</div>
|
|
9367
10248
|
`,
|
|
9368
10249
|
}]
|
|
9369
|
-
}], propDecorators: {
|
|
9370
|
-
|
|
9371
|
-
var collectionView_page = /*#__PURE__*/Object.freeze({
|
|
9372
|
-
__proto__: null,
|
|
9373
|
-
CollectionViewPage: CollectionViewPage
|
|
9374
|
-
});
|
|
10250
|
+
}], ctorParameters: () => [], propDecorators: { preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: true }] }], documentData: [{ type: i0.Input, args: [{ isSignal: true, alias: "documentData", required: true }] }], collectionSlug: [{ type: i0.Input, args: [{ isSignal: true, alias: "collectionSlug", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], editBlockRequest: [{ type: i0.Output, args: ["editBlockRequest"] }], previewIframe: [{ type: i0.ViewChild, args: ['previewIframe', { isSignal: true }] }] } });
|
|
9375
10251
|
|
|
9376
10252
|
/**
|
|
9377
10253
|
* Block Edit Dialog
|
|
@@ -10432,140 +11308,6 @@ var setup_page = /*#__PURE__*/Object.freeze({
|
|
|
10432
11308
|
SetupPage: SetupPage
|
|
10433
11309
|
});
|
|
10434
11310
|
|
|
10435
|
-
/**
|
|
10436
|
-
* Media Preview Component
|
|
10437
|
-
*
|
|
10438
|
-
* Displays a preview of media based on its type:
|
|
10439
|
-
* - Images: Thumbnail preview
|
|
10440
|
-
* - Videos: Video icon with optional poster
|
|
10441
|
-
* - Audio: Audio icon
|
|
10442
|
-
* - Documents: Document icon
|
|
10443
|
-
* - Other: Generic file icon
|
|
10444
|
-
*
|
|
10445
|
-
* @example
|
|
10446
|
-
* ```html
|
|
10447
|
-
* <mcms-media-preview
|
|
10448
|
-
* [media]="mediaDocument"
|
|
10449
|
-
* [size]="'md'"
|
|
10450
|
-
* />
|
|
10451
|
-
* ```
|
|
10452
|
-
*/
|
|
10453
|
-
class MediaPreviewComponent {
|
|
10454
|
-
/** Media data to preview */
|
|
10455
|
-
media = input(null, ...(ngDevMode ? [{ debugName: "media" }] : []));
|
|
10456
|
-
/** Size of the preview */
|
|
10457
|
-
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
10458
|
-
/** Custom class override */
|
|
10459
|
-
class = input('', ...(ngDevMode ? [{ debugName: "class" }] : []));
|
|
10460
|
-
/** Whether to show rounded corners */
|
|
10461
|
-
rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : []));
|
|
10462
|
-
/** Host classes */
|
|
10463
|
-
hostClasses = computed(() => {
|
|
10464
|
-
const sizeClass = this.sizeClasses()[this.size()];
|
|
10465
|
-
const roundedClass = this.rounded() ? 'rounded-md overflow-hidden' : '';
|
|
10466
|
-
return `inline-block ${sizeClass} ${roundedClass} ${this.class()}`;
|
|
10467
|
-
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
10468
|
-
/** Size classes map */
|
|
10469
|
-
sizeClasses = computed(() => ({
|
|
10470
|
-
xs: 'h-8 w-8',
|
|
10471
|
-
sm: 'h-12 w-12',
|
|
10472
|
-
md: 'h-20 w-20',
|
|
10473
|
-
lg: 'h-32 w-32',
|
|
10474
|
-
xl: 'h-48 w-48',
|
|
10475
|
-
}), ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
|
|
10476
|
-
/** Icon size classes */
|
|
10477
|
-
iconClasses = computed(() => {
|
|
10478
|
-
const sizes = {
|
|
10479
|
-
xs: 'text-lg',
|
|
10480
|
-
sm: 'text-xl',
|
|
10481
|
-
md: 'text-3xl',
|
|
10482
|
-
lg: 'text-4xl',
|
|
10483
|
-
xl: 'text-6xl',
|
|
10484
|
-
};
|
|
10485
|
-
return `${sizes[this.size()]} text-mcms-muted-foreground`;
|
|
10486
|
-
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
10487
|
-
/** Whether the media is an image */
|
|
10488
|
-
isImage = computed(() => {
|
|
10489
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10490
|
-
return mimeType.startsWith('image/');
|
|
10491
|
-
}, ...(ngDevMode ? [{ debugName: "isImage" }] : []));
|
|
10492
|
-
/** Whether the media is a video */
|
|
10493
|
-
isVideo = computed(() => {
|
|
10494
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10495
|
-
return mimeType.startsWith('video/');
|
|
10496
|
-
}, ...(ngDevMode ? [{ debugName: "isVideo" }] : []));
|
|
10497
|
-
/** Whether the media is audio */
|
|
10498
|
-
isAudio = computed(() => {
|
|
10499
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10500
|
-
return mimeType.startsWith('audio/');
|
|
10501
|
-
}, ...(ngDevMode ? [{ debugName: "isAudio" }] : []));
|
|
10502
|
-
/** Image URL for preview */
|
|
10503
|
-
imageUrl = computed(() => {
|
|
10504
|
-
const media = this.media();
|
|
10505
|
-
if (!media)
|
|
10506
|
-
return '';
|
|
10507
|
-
return media.url ?? `/api/media/file/${media.path}`;
|
|
10508
|
-
}, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
|
|
10509
|
-
/** Icon name based on media type */
|
|
10510
|
-
iconName = computed(() => {
|
|
10511
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10512
|
-
if (mimeType.startsWith('video/')) {
|
|
10513
|
-
return heroFilm;
|
|
10514
|
-
}
|
|
10515
|
-
if (mimeType.startsWith('audio/')) {
|
|
10516
|
-
return heroMusicalNote;
|
|
10517
|
-
}
|
|
10518
|
-
if (mimeType === 'application/pdf') {
|
|
10519
|
-
return heroDocumentText;
|
|
10520
|
-
}
|
|
10521
|
-
if (mimeType.startsWith('application/zip') || mimeType.includes('compressed')) {
|
|
10522
|
-
return heroArchiveBox;
|
|
10523
|
-
}
|
|
10524
|
-
if (mimeType.startsWith('image/')) {
|
|
10525
|
-
return heroPhoto;
|
|
10526
|
-
}
|
|
10527
|
-
return heroDocument;
|
|
10528
|
-
}, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
|
|
10529
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10530
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", 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: `
|
|
10531
|
-
@if (isImage()) {
|
|
10532
|
-
<img
|
|
10533
|
-
[src]="imageUrl()"
|
|
10534
|
-
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
10535
|
-
class="h-full w-full object-cover"
|
|
10536
|
-
/>
|
|
10537
|
-
} @else {
|
|
10538
|
-
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
10539
|
-
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
10540
|
-
</div>
|
|
10541
|
-
}
|
|
10542
|
-
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10543
|
-
}
|
|
10544
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, decorators: [{
|
|
10545
|
-
type: Component,
|
|
10546
|
-
args: [{
|
|
10547
|
-
selector: 'mcms-media-preview',
|
|
10548
|
-
host: {
|
|
10549
|
-
'[class]': 'hostClasses()',
|
|
10550
|
-
},
|
|
10551
|
-
template: `
|
|
10552
|
-
@if (isImage()) {
|
|
10553
|
-
<img
|
|
10554
|
-
[src]="imageUrl()"
|
|
10555
|
-
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
10556
|
-
class="h-full w-full object-cover"
|
|
10557
|
-
/>
|
|
10558
|
-
} @else {
|
|
10559
|
-
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
10560
|
-
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
10561
|
-
</div>
|
|
10562
|
-
}
|
|
10563
|
-
`,
|
|
10564
|
-
imports: [NgIcon],
|
|
10565
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10566
|
-
}]
|
|
10567
|
-
}], propDecorators: { media: [{ type: i0.Input, args: [{ isSignal: true, alias: "media", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }] } });
|
|
10568
|
-
|
|
10569
11311
|
/**
|
|
10570
11312
|
* Type guard to check if a value has the shape of a MediaEditItem.
|
|
10571
11313
|
*/
|
|
@@ -12593,18 +13335,18 @@ function provideMomentumFieldRenderers() {
|
|
|
12593
13335
|
registry.register('checkbox', () => Promise.resolve().then(function () { return checkboxField_component; }).then((m) => m.CheckboxFieldRenderer));
|
|
12594
13336
|
registry.register('date', () => Promise.resolve().then(function () { return dateField_component; }).then((m) => m.DateFieldRenderer));
|
|
12595
13337
|
registry.register('upload', () => Promise.resolve().then(function () { return uploadField_component; }).then((m) => m.UploadFieldRenderer));
|
|
12596
|
-
registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-
|
|
13338
|
+
registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-BC8pRU89.mjs').then((m) => m.RichTextFieldRenderer));
|
|
12597
13339
|
// Layout field renderers (support nested field rendering)
|
|
12598
|
-
registry.register('group', () => import('./momentumcms-admin-group-field.component-
|
|
12599
|
-
registry.register('array', () => import('./momentumcms-admin-array-field.component-
|
|
12600
|
-
registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-
|
|
13340
|
+
registry.register('group', () => import('./momentumcms-admin-group-field.component-B48_zbo0.mjs').then((m) => m.GroupFieldRenderer));
|
|
13341
|
+
registry.register('array', () => import('./momentumcms-admin-array-field.component-Bjlcczwg.mjs').then((m) => m.ArrayFieldRenderer));
|
|
13342
|
+
registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-4vLqDGbB.mjs').then((m) => m.BlocksFieldRenderer));
|
|
12601
13343
|
// Visual block editor variant (blocks field with admin.editor === 'visual')
|
|
12602
13344
|
registry.register('blocks-visual', () => Promise.resolve().then(function () { return visualBlockEditor_component; }).then((m) => m.VisualBlockEditorComponent));
|
|
12603
|
-
registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-
|
|
13345
|
+
registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-D-UQgd7m.mjs').then((m) => m.RelationshipFieldRenderer));
|
|
12604
13346
|
// Layout-only renderers (tabs, collapsible, row)
|
|
12605
|
-
registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-
|
|
12606
|
-
registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-
|
|
12607
|
-
registry.register('row', () => import('./momentumcms-admin-row-field.component
|
|
13347
|
+
registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-B4X73eCM.mjs').then((m) => m.TabsFieldRenderer));
|
|
13348
|
+
registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-63-9kSgm.mjs').then((m) => m.CollapsibleFieldRenderer));
|
|
13349
|
+
registry.register('row', () => import('./momentumcms-admin-row-field.component--EOPGDtM.mjs').then((m) => m.RowFieldRenderer));
|
|
12608
13350
|
};
|
|
12609
13351
|
},
|
|
12610
13352
|
},
|
|
@@ -13446,39 +14188,39 @@ class MediaPickerDialog {
|
|
|
13446
14188
|
this.isLoading.set(true);
|
|
13447
14189
|
try {
|
|
13448
14190
|
const collection = this.api.collection(this.collectionSlug());
|
|
13449
|
-
|
|
13450
|
-
|
|
14191
|
+
// Fetch all media — DB adapter does not support complex where operators
|
|
14192
|
+
const result = await collection.find({
|
|
14193
|
+
page,
|
|
14194
|
+
limit: this.limit(),
|
|
14195
|
+
});
|
|
14196
|
+
let items = toMediaItems(result.docs);
|
|
14197
|
+
// Client-side search filter
|
|
13451
14198
|
if (search) {
|
|
13452
|
-
|
|
14199
|
+
const lowerSearch = search.toLowerCase();
|
|
14200
|
+
items = items.filter((m) => m.filename.toLowerCase().includes(lowerSearch));
|
|
13453
14201
|
}
|
|
13454
|
-
//
|
|
14202
|
+
// Client-side MIME type filter
|
|
13455
14203
|
const mimeTypes = this.data?.mimeTypes;
|
|
13456
14204
|
if (mimeTypes && mimeTypes.length > 0) {
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
if (prefixes.length > 0 || exactTypes.length > 0) {
|
|
13461
|
-
const orConditions = [];
|
|
13462
|
-
for (const prefix of prefixes) {
|
|
13463
|
-
orConditions.push({ mimeType: { startsWith: prefix } });
|
|
13464
|
-
}
|
|
13465
|
-
for (const type of exactTypes) {
|
|
13466
|
-
orConditions.push({ mimeType: { equals: type } });
|
|
14205
|
+
items = items.filter((m) => mimeTypes.some((pattern) => {
|
|
14206
|
+
if (pattern.endsWith('/*')) {
|
|
14207
|
+
return m.mimeType.startsWith(pattern.slice(0, -1));
|
|
13467
14208
|
}
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
14209
|
+
return m.mimeType === pattern;
|
|
14210
|
+
}));
|
|
14211
|
+
}
|
|
14212
|
+
this.mediaItems.set(items);
|
|
14213
|
+
// When client-side filtering is active, use filtered counts
|
|
14214
|
+
// to avoid pagination showing incorrect totals
|
|
14215
|
+
const hasClientFilter = !!search || (mimeTypes && mimeTypes.length > 0);
|
|
14216
|
+
if (hasClientFilter) {
|
|
14217
|
+
this.totalDocs.set(items.length);
|
|
14218
|
+
this.totalPages.set(1); // Client-filtered results are always a single page
|
|
14219
|
+
}
|
|
14220
|
+
else {
|
|
14221
|
+
this.totalDocs.set(result.totalDocs);
|
|
14222
|
+
this.totalPages.set(result.totalPages);
|
|
13472
14223
|
}
|
|
13473
|
-
const result = await collection.find({
|
|
13474
|
-
where: Object.keys(whereClause).length > 0 ? whereClause : undefined,
|
|
13475
|
-
page,
|
|
13476
|
-
limit: this.limit(),
|
|
13477
|
-
sort: '-createdAt',
|
|
13478
|
-
});
|
|
13479
|
-
this.mediaItems.set(toMediaItems(result.docs));
|
|
13480
|
-
this.totalDocs.set(result.totalDocs);
|
|
13481
|
-
this.totalPages.set(result.totalPages);
|
|
13482
14224
|
}
|
|
13483
14225
|
catch (error) {
|
|
13484
14226
|
console.error('Failed to load media:', error);
|
|
@@ -13751,9 +14493,10 @@ function getInputFromEvent(event) {
|
|
|
13751
14493
|
* a FieldTree node's FieldState rather than event-based I/O.
|
|
13752
14494
|
*/
|
|
13753
14495
|
class UploadFieldRenderer {
|
|
13754
|
-
|
|
14496
|
+
fileInputRef = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInputRef" }] : []));
|
|
13755
14497
|
uploadService = inject(UploadService);
|
|
13756
14498
|
dialogService = inject(DialogService);
|
|
14499
|
+
api = injectMomentumAPI();
|
|
13757
14500
|
/** Field definition */
|
|
13758
14501
|
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
13759
14502
|
/** Signal forms FieldTree node for this field */
|
|
@@ -13775,6 +14518,43 @@ class UploadFieldRenderer {
|
|
|
13775
14518
|
uploadingFilename = signal('', ...(ngDevMode ? [{ debugName: "uploadingFilename" }] : []));
|
|
13776
14519
|
uploadError = signal(null, ...(ngDevMode ? [{ debugName: "uploadError" }] : []));
|
|
13777
14520
|
uploadingFile = signal(null, ...(ngDevMode ? [{ debugName: "uploadingFile" }] : []));
|
|
14521
|
+
/** Resolved media document fetched from API when value is just an ID string */
|
|
14522
|
+
resolvedMedia = signal(null, ...(ngDevMode ? [{ debugName: "resolvedMedia" }] : []));
|
|
14523
|
+
resolvedMediaId = null;
|
|
14524
|
+
constructor() {
|
|
14525
|
+
// When the value is a string (media ID), fetch the full media document for preview
|
|
14526
|
+
effect(() => {
|
|
14527
|
+
const val = this.currentValue();
|
|
14528
|
+
if (typeof val === 'string' && val !== '') {
|
|
14529
|
+
// Avoid re-fetching the same ID
|
|
14530
|
+
const id = val;
|
|
14531
|
+
if (untracked(() => this.resolvedMediaId) === id)
|
|
14532
|
+
return;
|
|
14533
|
+
this.resolvedMediaId = id;
|
|
14534
|
+
const relationTo = untracked(() => this.uploadField().relationTo);
|
|
14535
|
+
this.api
|
|
14536
|
+
.collection(relationTo)
|
|
14537
|
+
.findById(id)
|
|
14538
|
+
.then((doc) => {
|
|
14539
|
+
if (doc) {
|
|
14540
|
+
this.resolvedMedia.set(doc);
|
|
14541
|
+
}
|
|
14542
|
+
})
|
|
14543
|
+
.catch(() => {
|
|
14544
|
+
// Silently fail — preview will show placeholder
|
|
14545
|
+
});
|
|
14546
|
+
}
|
|
14547
|
+
else if (typeof val === 'object' && val !== null) {
|
|
14548
|
+
// Already a full document, clear any stale resolved data
|
|
14549
|
+
this.resolvedMedia.set(null);
|
|
14550
|
+
this.resolvedMediaId = null;
|
|
14551
|
+
}
|
|
14552
|
+
else {
|
|
14553
|
+
this.resolvedMedia.set(null);
|
|
14554
|
+
this.resolvedMediaId = null;
|
|
14555
|
+
}
|
|
14556
|
+
});
|
|
14557
|
+
}
|
|
13778
14558
|
/** Unique field ID */
|
|
13779
14559
|
fieldId = computed(() => `field-${this.path().replace(/\./g, '-')}`, ...(ngDevMode ? [{ debugName: "fieldId" }] : []));
|
|
13780
14560
|
/** Computed label */
|
|
@@ -13804,32 +14584,35 @@ class UploadFieldRenderer {
|
|
|
13804
14584
|
const val = this.currentValue();
|
|
13805
14585
|
return val !== null && val !== undefined && val !== '';
|
|
13806
14586
|
}, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
|
|
14587
|
+
/** Effective media document — full object from value, or resolved from API */
|
|
14588
|
+
effectiveMedia = computed(() => {
|
|
14589
|
+
const val = this.currentValue();
|
|
14590
|
+
if (typeof val === 'object' && val !== null)
|
|
14591
|
+
return val;
|
|
14592
|
+
// Value is a string ID — use resolved media if available
|
|
14593
|
+
return this.resolvedMedia();
|
|
14594
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveMedia" }] : []));
|
|
13807
14595
|
/** Media preview data from value */
|
|
13808
14596
|
mediaPreviewData = computed(() => {
|
|
13809
|
-
const
|
|
13810
|
-
if (!
|
|
14597
|
+
const media = this.effectiveMedia();
|
|
14598
|
+
if (!media)
|
|
13811
14599
|
return null;
|
|
13812
|
-
|
|
13813
|
-
if (typeof val === 'object' && val !== null) {
|
|
14600
|
+
if (typeof media === 'object' && media !== null) {
|
|
13814
14601
|
return {
|
|
13815
|
-
url: getStringProp(
|
|
13816
|
-
path: getStringProp(
|
|
13817
|
-
mimeType: getStringProp(
|
|
13818
|
-
filename: getStringProp(
|
|
13819
|
-
alt: getStringProp(
|
|
14602
|
+
url: getStringProp(media, 'url'),
|
|
14603
|
+
path: getStringProp(media, 'path'),
|
|
14604
|
+
mimeType: getStringProp(media, 'mimeType'),
|
|
14605
|
+
filename: getStringProp(media, 'filename'),
|
|
14606
|
+
alt: getStringProp(media, 'alt'),
|
|
13820
14607
|
};
|
|
13821
14608
|
}
|
|
13822
|
-
|
|
13823
|
-
// Return a placeholder
|
|
13824
|
-
return {
|
|
13825
|
-
path: String(val),
|
|
13826
|
-
};
|
|
14609
|
+
return null;
|
|
13827
14610
|
}, ...(ngDevMode ? [{ debugName: "mediaPreviewData" }] : []));
|
|
13828
14611
|
/** Media filename from value */
|
|
13829
14612
|
mediaFilename = computed(() => {
|
|
13830
|
-
const
|
|
13831
|
-
if (typeof
|
|
13832
|
-
return getStringProp(
|
|
14613
|
+
const media = this.effectiveMedia();
|
|
14614
|
+
if (typeof media === 'object' && media !== null) {
|
|
14615
|
+
return getStringProp(media, 'filename') ?? 'Selected media';
|
|
13833
14616
|
}
|
|
13834
14617
|
return 'Selected media';
|
|
13835
14618
|
}, ...(ngDevMode ? [{ debugName: "mediaFilename" }] : []));
|
|
@@ -13930,11 +14713,19 @@ class UploadFieldRenderer {
|
|
|
13930
14713
|
triggerFileInput() {
|
|
13931
14714
|
if (this.isDisabled())
|
|
13932
14715
|
return;
|
|
13933
|
-
const
|
|
13934
|
-
if (
|
|
13935
|
-
|
|
14716
|
+
const ref = this.fileInputRef();
|
|
14717
|
+
if (ref) {
|
|
14718
|
+
ref.nativeElement.click();
|
|
13936
14719
|
}
|
|
13937
14720
|
}
|
|
14721
|
+
/**
|
|
14722
|
+
* Handle Space keydown on drop zone.
|
|
14723
|
+
* Prevents default scroll behavior and triggers file input.
|
|
14724
|
+
*/
|
|
14725
|
+
onDropZoneSpace(event) {
|
|
14726
|
+
event.preventDefault();
|
|
14727
|
+
this.triggerFileInput();
|
|
14728
|
+
}
|
|
13938
14729
|
/**
|
|
13939
14730
|
* Handle file selection from input.
|
|
13940
14731
|
*/
|
|
@@ -13979,7 +14770,8 @@ class UploadFieldRenderer {
|
|
|
13979
14770
|
this.uploadProgress.set(0);
|
|
13980
14771
|
this.uploadingFilename.set(file.name);
|
|
13981
14772
|
this.uploadingFile.set(file);
|
|
13982
|
-
this.
|
|
14773
|
+
const relationTo = this.uploadField().relationTo;
|
|
14774
|
+
this.uploadService.uploadToCollection(relationTo, file).subscribe({
|
|
13983
14775
|
next: (progress) => {
|
|
13984
14776
|
this.uploadProgress.set(progress.progress);
|
|
13985
14777
|
if (progress.status === 'complete' && progress.result) {
|
|
@@ -14037,7 +14829,7 @@ class UploadFieldRenderer {
|
|
|
14037
14829
|
}
|
|
14038
14830
|
}
|
|
14039
14831
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: UploadFieldRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14040
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: UploadFieldRenderer, isStandalone: true, selector: "mcms-upload-field-renderer", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, formNode: { classPropertyName: "formNode", publicName: "formNode", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
14832
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: UploadFieldRenderer, isStandalone: true, selector: "mcms-upload-field-renderer", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, formNode: { classPropertyName: "formNode", publicName: "formNode", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "fileInputRef", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
14041
14833
|
<mcms-form-field
|
|
14042
14834
|
[id]="fieldId()"
|
|
14043
14835
|
[required]="required()"
|
|
@@ -14095,7 +14887,7 @@ class UploadFieldRenderer {
|
|
|
14095
14887
|
</div>
|
|
14096
14888
|
</div>
|
|
14097
14889
|
} @else {
|
|
14098
|
-
<!-- Drop zone -->
|
|
14890
|
+
<!-- Drop zone — keyboard-accessible for WCAG 2.1 SC 2.1.1 -->
|
|
14099
14891
|
<div
|
|
14100
14892
|
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
14101
14893
|
[class.border-mcms-border]="!isDragging()"
|
|
@@ -14103,16 +14895,16 @@ class UploadFieldRenderer {
|
|
|
14103
14895
|
[class.bg-mcms-primary/5]="isDragging()"
|
|
14104
14896
|
[class.cursor-pointer]="!isDisabled()"
|
|
14105
14897
|
[class.opacity-50]="isDisabled()"
|
|
14106
|
-
tabindex="0"
|
|
14107
14898
|
role="button"
|
|
14899
|
+
tabindex="0"
|
|
14900
|
+
[attr.aria-label]="'Upload file for ' + label()"
|
|
14108
14901
|
[attr.aria-disabled]="isDisabled()"
|
|
14109
|
-
[attr.aria-label]="'Upload ' + label() + ' file. Drag and drop or click to browse.'"
|
|
14110
14902
|
(dragover)="onDragOver($event)"
|
|
14111
14903
|
(dragleave)="onDragLeave($event)"
|
|
14112
14904
|
(drop)="onDrop($event)"
|
|
14113
14905
|
(click)="triggerFileInput()"
|
|
14114
14906
|
(keydown.enter)="triggerFileInput()"
|
|
14115
|
-
(keydown.space)="
|
|
14907
|
+
(keydown.space)="onDropZoneSpace($event)"
|
|
14116
14908
|
>
|
|
14117
14909
|
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
14118
14910
|
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
@@ -14135,31 +14927,31 @@ class UploadFieldRenderer {
|
|
|
14135
14927
|
</p>
|
|
14136
14928
|
}
|
|
14137
14929
|
</div>
|
|
14138
|
-
@if (!isDisabled()) {
|
|
14139
|
-
<div class="mt-2 flex gap-2">
|
|
14140
|
-
<button
|
|
14141
|
-
mcms-button
|
|
14142
|
-
variant="outline"
|
|
14143
|
-
size="sm"
|
|
14144
|
-
type="button"
|
|
14145
|
-
(click)="$event.stopPropagation(); openMediaPicker()"
|
|
14146
|
-
>
|
|
14147
|
-
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
14148
|
-
Select from library
|
|
14149
|
-
</button>
|
|
14150
|
-
</div>
|
|
14151
|
-
}
|
|
14152
14930
|
</div>
|
|
14153
|
-
<input
|
|
14154
|
-
#fileInput
|
|
14155
|
-
type="file"
|
|
14156
|
-
class="sr-only"
|
|
14157
|
-
[accept]="acceptAttribute()"
|
|
14158
|
-
[disabled]="isDisabled()"
|
|
14159
|
-
(change)="onFileSelected($event)"
|
|
14160
|
-
[attr.aria-label]="'Choose file for ' + label()"
|
|
14161
|
-
/>
|
|
14162
14931
|
</div>
|
|
14932
|
+
@if (!isDisabled()) {
|
|
14933
|
+
<div class="mt-2 flex gap-2">
|
|
14934
|
+
<button
|
|
14935
|
+
mcms-button
|
|
14936
|
+
variant="outline"
|
|
14937
|
+
size="sm"
|
|
14938
|
+
type="button"
|
|
14939
|
+
(click)="openMediaPicker()"
|
|
14940
|
+
>
|
|
14941
|
+
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
14942
|
+
Select from library
|
|
14943
|
+
</button>
|
|
14944
|
+
</div>
|
|
14945
|
+
}
|
|
14946
|
+
<input
|
|
14947
|
+
#fileInput
|
|
14948
|
+
type="file"
|
|
14949
|
+
class="sr-only"
|
|
14950
|
+
[accept]="acceptAttribute()"
|
|
14951
|
+
[disabled]="isDisabled()"
|
|
14952
|
+
(change)="onFileSelected($event)"
|
|
14953
|
+
[attr.aria-label]="'Choose file for ' + label()"
|
|
14954
|
+
/>
|
|
14163
14955
|
}
|
|
14164
14956
|
|
|
14165
14957
|
@if (uploadError()) {
|
|
@@ -14233,7 +15025,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14233
15025
|
</div>
|
|
14234
15026
|
</div>
|
|
14235
15027
|
} @else {
|
|
14236
|
-
<!-- Drop zone -->
|
|
15028
|
+
<!-- Drop zone — keyboard-accessible for WCAG 2.1 SC 2.1.1 -->
|
|
14237
15029
|
<div
|
|
14238
15030
|
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
14239
15031
|
[class.border-mcms-border]="!isDragging()"
|
|
@@ -14241,16 +15033,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14241
15033
|
[class.bg-mcms-primary/5]="isDragging()"
|
|
14242
15034
|
[class.cursor-pointer]="!isDisabled()"
|
|
14243
15035
|
[class.opacity-50]="isDisabled()"
|
|
14244
|
-
tabindex="0"
|
|
14245
15036
|
role="button"
|
|
15037
|
+
tabindex="0"
|
|
15038
|
+
[attr.aria-label]="'Upload file for ' + label()"
|
|
14246
15039
|
[attr.aria-disabled]="isDisabled()"
|
|
14247
|
-
[attr.aria-label]="'Upload ' + label() + ' file. Drag and drop or click to browse.'"
|
|
14248
15040
|
(dragover)="onDragOver($event)"
|
|
14249
15041
|
(dragleave)="onDragLeave($event)"
|
|
14250
15042
|
(drop)="onDrop($event)"
|
|
14251
15043
|
(click)="triggerFileInput()"
|
|
14252
15044
|
(keydown.enter)="triggerFileInput()"
|
|
14253
|
-
(keydown.space)="
|
|
15045
|
+
(keydown.space)="onDropZoneSpace($event)"
|
|
14254
15046
|
>
|
|
14255
15047
|
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
14256
15048
|
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
@@ -14273,31 +15065,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14273
15065
|
</p>
|
|
14274
15066
|
}
|
|
14275
15067
|
</div>
|
|
14276
|
-
@if (!isDisabled()) {
|
|
14277
|
-
<div class="mt-2 flex gap-2">
|
|
14278
|
-
<button
|
|
14279
|
-
mcms-button
|
|
14280
|
-
variant="outline"
|
|
14281
|
-
size="sm"
|
|
14282
|
-
type="button"
|
|
14283
|
-
(click)="$event.stopPropagation(); openMediaPicker()"
|
|
14284
|
-
>
|
|
14285
|
-
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
14286
|
-
Select from library
|
|
14287
|
-
</button>
|
|
14288
|
-
</div>
|
|
14289
|
-
}
|
|
14290
15068
|
</div>
|
|
14291
|
-
<input
|
|
14292
|
-
#fileInput
|
|
14293
|
-
type="file"
|
|
14294
|
-
class="sr-only"
|
|
14295
|
-
[accept]="acceptAttribute()"
|
|
14296
|
-
[disabled]="isDisabled()"
|
|
14297
|
-
(change)="onFileSelected($event)"
|
|
14298
|
-
[attr.aria-label]="'Choose file for ' + label()"
|
|
14299
|
-
/>
|
|
14300
15069
|
</div>
|
|
15070
|
+
@if (!isDisabled()) {
|
|
15071
|
+
<div class="mt-2 flex gap-2">
|
|
15072
|
+
<button
|
|
15073
|
+
mcms-button
|
|
15074
|
+
variant="outline"
|
|
15075
|
+
size="sm"
|
|
15076
|
+
type="button"
|
|
15077
|
+
(click)="openMediaPicker()"
|
|
15078
|
+
>
|
|
15079
|
+
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
15080
|
+
Select from library
|
|
15081
|
+
</button>
|
|
15082
|
+
</div>
|
|
15083
|
+
}
|
|
15084
|
+
<input
|
|
15085
|
+
#fileInput
|
|
15086
|
+
type="file"
|
|
15087
|
+
class="sr-only"
|
|
15088
|
+
[accept]="acceptAttribute()"
|
|
15089
|
+
[disabled]="isDisabled()"
|
|
15090
|
+
(change)="onFileSelected($event)"
|
|
15091
|
+
[attr.aria-label]="'Choose file for ' + label()"
|
|
15092
|
+
/>
|
|
14301
15093
|
}
|
|
14302
15094
|
|
|
14303
15095
|
@if (uploadError()) {
|
|
@@ -14306,7 +15098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14306
15098
|
</mcms-form-field>
|
|
14307
15099
|
`,
|
|
14308
15100
|
}]
|
|
14309
|
-
}], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], formNode: [{ type: i0.Input, args: [{ isSignal: true, alias: "formNode", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }] } });
|
|
15101
|
+
}], ctorParameters: () => [], propDecorators: { fileInputRef: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], formNode: [{ type: i0.Input, args: [{ isSignal: true, alias: "formNode", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }] } });
|
|
14310
15102
|
|
|
14311
15103
|
var uploadField_component = /*#__PURE__*/Object.freeze({
|
|
14312
15104
|
__proto__: null,
|
|
@@ -14320,4 +15112,4 @@ var uploadField_component = /*#__PURE__*/Object.freeze({
|
|
|
14320
15112
|
*/
|
|
14321
15113
|
|
|
14322
15114
|
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 };
|
|
14323
|
-
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-
|
|
15115
|
+
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-D_47TVaR.mjs.map
|