@momentumcms/admin 0.2.0 → 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-CT5NlIEv.mjs → momentumcms-admin-array-field.component-Bjlcczwg.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-array-field.component-CT5NlIEv.mjs.map → momentumcms-admin-array-field.component-Bjlcczwg.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-blocks-field.component-Cz7HmuBK.mjs → momentumcms-admin-blocks-field.component-4vLqDGbB.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-blocks-field.component-Cz7HmuBK.mjs.map → momentumcms-admin-blocks-field.component-4vLqDGbB.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-collapsible-field.component-CtwrGQvg.mjs → momentumcms-admin-collapsible-field.component-63-9kSgm.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-collapsible-field.component-CtwrGQvg.mjs.map → momentumcms-admin-collapsible-field.component-63-9kSgm.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-global-edit.page-BBUtWCSl.mjs → momentumcms-admin-global-edit.page-DSnkwdgn.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-global-edit.page-BBUtWCSl.mjs.map → momentumcms-admin-global-edit.page-DSnkwdgn.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-group-field.component-BZeG8Oqy.mjs → momentumcms-admin-group-field.component-B48_zbo0.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-group-field.component-BZeG8Oqy.mjs.map → momentumcms-admin-group-field.component-B48_zbo0.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-momentumcms-admin-o0FbJXZN.mjs → momentumcms-admin-momentumcms-admin-D_47TVaR.mjs} +1531 -749
- package/fesm2022/momentumcms-admin-momentumcms-admin-D_47TVaR.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-relationship-field.component-BuxtRs2_.mjs → momentumcms-admin-relationship-field.component-D-UQgd7m.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-relationship-field.component-BuxtRs2_.mjs.map → momentumcms-admin-relationship-field.component-D-UQgd7m.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-rich-text-field.component-DKQ6pwp7.mjs → momentumcms-admin-rich-text-field.component-BC8pRU89.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-rich-text-field.component-DKQ6pwp7.mjs.map → momentumcms-admin-rich-text-field.component-BC8pRU89.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-row-field.component-ks3FXd4B.mjs → momentumcms-admin-row-field.component--EOPGDtM.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-row-field.component-ks3FXd4B.mjs.map → momentumcms-admin-row-field.component--EOPGDtM.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-tabs-field.component-mZ4dpZoD.mjs → momentumcms-admin-tabs-field.component-B4X73eCM.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-tabs-field.component-mZ4dpZoD.mjs.map → momentumcms-admin-tabs-field.component-B4X73eCM.mjs.map} +1 -1
- package/fesm2022/momentumcms-admin.mjs +1 -1
- package/package.json +1 -1
- package/types/momentumcms-admin.d.ts +92 -29
- package/fesm2022/momentumcms-admin-momentumcms-admin-o0FbJXZN.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
|
*
|
|
@@ -4129,121 +4217,739 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4129
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"] }] } });
|
|
4130
4218
|
|
|
4131
4219
|
/**
|
|
4132
|
-
*
|
|
4220
|
+
* Media Preview Component
|
|
4133
4221
|
*
|
|
4134
|
-
*
|
|
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
|
|
4135
4228
|
*
|
|
4136
4229
|
* @example
|
|
4137
4230
|
* ```html
|
|
4138
|
-
* <mcms-
|
|
4139
|
-
* [
|
|
4140
|
-
*
|
|
4141
|
-
* mode="edit"
|
|
4142
|
-
* (saved)="onSaved($event)"
|
|
4143
|
-
* (cancelled)="onCancel()"
|
|
4231
|
+
* <mcms-media-preview
|
|
4232
|
+
* [media]="mediaDocument"
|
|
4233
|
+
* [size]="'md'"
|
|
4144
4234
|
* />
|
|
4145
4235
|
* ```
|
|
4146
4236
|
*/
|
|
4147
|
-
class
|
|
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
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
dashboardPath = computed(() => {
|
|
4209
|
-
const base = this.basePath();
|
|
4210
|
-
return base.replace(/\/collections$/, '');
|
|
4211
|
-
}, ...(ngDevMode ? [{ debugName: "dashboardPath" }] : []));
|
|
4212
|
-
/** Collection list path */
|
|
4213
|
-
collectionListPath = computed(() => {
|
|
4214
|
-
return `${this.basePath()}/${this.collection().slug}`;
|
|
4215
|
-
}, ...(ngDevMode ? [{ debugName: "collectionListPath" }] : []));
|
|
4216
|
-
/** Page title for breadcrumb */
|
|
4217
|
-
pageTitle = computed(() => {
|
|
4218
|
-
if (this.isGlobal()) {
|
|
4219
|
-
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;
|
|
4220
4298
|
}
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
return `Create ${this.collectionLabelSingular()}`;
|
|
4299
|
+
if (mimeType.startsWith('audio/')) {
|
|
4300
|
+
return heroMusicalNote;
|
|
4224
4301
|
}
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
for (const field of titleFields) {
|
|
4228
|
-
if (data[field] && typeof data[field] === 'string') {
|
|
4229
|
-
return data[field];
|
|
4230
|
-
}
|
|
4302
|
+
if (mimeType === 'application/pdf') {
|
|
4303
|
+
return heroDocumentText;
|
|
4231
4304
|
}
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
return
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
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) => {
|
|
4945
|
+
if (field.admin?.hidden)
|
|
4946
|
+
return false;
|
|
4947
|
+
if (field.admin?.condition && !field.admin.condition(data))
|
|
4948
|
+
return false;
|
|
4949
|
+
return true;
|
|
4950
|
+
});
|
|
4951
|
+
}, ...(ngDevMode ? [{ debugName: "visibleFields" }] : []));
|
|
4952
|
+
/** Whether user can edit */
|
|
4247
4953
|
canEdit = computed(() => {
|
|
4248
4954
|
return this.collectionAccess.canUpdate(this.collection().slug);
|
|
4249
4955
|
}, ...(ngDevMode ? [{ debugName: "canEdit" }] : []));
|
|
@@ -4364,9 +5070,62 @@ class EntityFormWidget {
|
|
|
4364
5070
|
this.isLoading.set(false);
|
|
4365
5071
|
}
|
|
4366
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
|
+
}
|
|
4367
5125
|
/**
|
|
4368
5126
|
* Handle form submission using Angular Signal Forms submit().
|
|
4369
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.
|
|
4370
5129
|
*/
|
|
4371
5130
|
async onSubmit() {
|
|
4372
5131
|
const ef = this.entityForm();
|
|
@@ -4379,7 +5138,7 @@ class EntityFormWidget {
|
|
|
4379
5138
|
this.formError.set(null);
|
|
4380
5139
|
try {
|
|
4381
5140
|
const slug = this.collection().slug;
|
|
4382
|
-
const data = this.formModel();
|
|
5141
|
+
const data = this.normalizeUploadFieldValues(this.formModel());
|
|
4383
5142
|
let result;
|
|
4384
5143
|
if (this.isGlobal()) {
|
|
4385
5144
|
// Global mode: always update (singleton)
|
|
@@ -4387,6 +5146,10 @@ class EntityFormWidget {
|
|
|
4387
5146
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4388
5147
|
result = await this.api.global(gSlug).update(data);
|
|
4389
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
|
+
}
|
|
4390
5153
|
else if (this.mode() === 'create') {
|
|
4391
5154
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4392
5155
|
result = await this.api.collection(slug).create(data);
|
|
@@ -4400,6 +5163,7 @@ class EntityFormWidget {
|
|
|
4400
5163
|
}
|
|
4401
5164
|
this.originalData.set(result);
|
|
4402
5165
|
this.formModel.set({ ...result });
|
|
5166
|
+
this.pendingFile.set(null);
|
|
4403
5167
|
ef().reset();
|
|
4404
5168
|
this.saved.emit(result);
|
|
4405
5169
|
if (!this.suppressNavigation() && !this.isGlobal()) {
|
|
@@ -4415,6 +5179,7 @@ class EntityFormWidget {
|
|
|
4415
5179
|
}
|
|
4416
5180
|
finally {
|
|
4417
5181
|
this.isSubmitting.set(false);
|
|
5182
|
+
this.isUploadingFile.set(false);
|
|
4418
5183
|
}
|
|
4419
5184
|
});
|
|
4420
5185
|
// submit() didn't call the callback — form is invalid
|
|
@@ -4423,6 +5188,85 @@ class EntityFormWidget {
|
|
|
4423
5188
|
void this.liveAnnouncer.announce('Form submission failed. Please fix the errors above before submitting.', 'assertive');
|
|
4424
5189
|
}
|
|
4425
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
|
+
}
|
|
4426
5270
|
/**
|
|
4427
5271
|
* Handle cancel.
|
|
4428
5272
|
*/
|
|
@@ -4532,6 +5376,20 @@ class EntityFormWidget {
|
|
|
4532
5376
|
</mcms-alert>
|
|
4533
5377
|
}
|
|
4534
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
|
+
|
|
4535
5393
|
<div class="space-y-6">
|
|
4536
5394
|
@for (field of visibleFields(); track field.name) {
|
|
4537
5395
|
<mcms-field-renderer
|
|
@@ -4600,7 +5458,7 @@ class EntityFormWidget {
|
|
|
4600
5458
|
</div>
|
|
4601
5459
|
}
|
|
4602
5460
|
</div>
|
|
4603
|
-
`, 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 });
|
|
4604
5462
|
}
|
|
4605
5463
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EntityFormWidget, decorators: [{
|
|
4606
5464
|
type: Component,
|
|
@@ -4618,6 +5476,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4618
5476
|
BreadcrumbItem,
|
|
4619
5477
|
BreadcrumbSeparator,
|
|
4620
5478
|
VersionHistoryWidget,
|
|
5479
|
+
CollectionUploadZoneComponent,
|
|
4621
5480
|
],
|
|
4622
5481
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4623
5482
|
host: { class: 'block' },
|
|
@@ -4679,6 +5538,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4679
5538
|
</mcms-alert>
|
|
4680
5539
|
}
|
|
4681
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
|
+
|
|
4682
5555
|
<div class="space-y-6">
|
|
4683
5556
|
@for (field of visibleFields(); track field.name) {
|
|
4684
5557
|
<mcms-field-renderer
|
|
@@ -5087,15 +5960,33 @@ class EntityViewWidget {
|
|
|
5087
5960
|
const e = this.entity();
|
|
5088
5961
|
if (!e || !col.admin?.preview)
|
|
5089
5962
|
return null;
|
|
5090
|
-
|
|
5963
|
+
const preview = col.admin.preview;
|
|
5964
|
+
if (typeof preview === 'function') {
|
|
5091
5965
|
try {
|
|
5092
5966
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T extends Entity with index signature
|
|
5093
|
-
return
|
|
5967
|
+
return preview(e);
|
|
5094
5968
|
}
|
|
5095
5969
|
catch {
|
|
5096
5970
|
return null;
|
|
5097
5971
|
}
|
|
5098
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
|
+
}
|
|
5099
5990
|
return null;
|
|
5100
5991
|
}, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
|
|
5101
5992
|
constructor() {
|
|
@@ -5327,44 +6218,64 @@ class EntityViewWidget {
|
|
|
5327
6218
|
this.loadEntity(this.collection().slug, this.entityId());
|
|
5328
6219
|
}
|
|
5329
6220
|
/**
|
|
5330
|
-
* Resolve relationship field values from IDs to display labels.
|
|
6221
|
+
* Resolve relationship and upload field values from IDs to display labels.
|
|
5331
6222
|
*/
|
|
5332
6223
|
resolveRelationships(entity) {
|
|
5333
6224
|
const fields = this.collection().fields;
|
|
5334
6225
|
const resolved = new Map();
|
|
5335
6226
|
const promises = [];
|
|
5336
6227
|
for (const field of fields) {
|
|
5337
|
-
if (field.type
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
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
|
+
}
|
|
5357
6248
|
}
|
|
6249
|
+
resolved.set(field.name, String(doc['id'] ?? rawValue));
|
|
5358
6250
|
}
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
6251
|
+
else {
|
|
6252
|
+
resolved.set(field.name, 'Unknown');
|
|
6253
|
+
}
|
|
6254
|
+
})
|
|
6255
|
+
.catch(() => {
|
|
5362
6256
|
resolved.set(field.name, 'Unknown');
|
|
5363
|
-
}
|
|
5364
|
-
}
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
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
|
+
}
|
|
5368
6279
|
}
|
|
5369
6280
|
if (promises.length > 0) {
|
|
5370
6281
|
Promise.all(promises).then(() => {
|
|
@@ -5890,6 +6801,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
5890
6801
|
}]
|
|
5891
6802
|
}] });
|
|
5892
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
|
+
|
|
5893
6845
|
/**
|
|
5894
6846
|
* Admin Sidebar Widget
|
|
5895
6847
|
*
|
|
@@ -5935,27 +6887,7 @@ class AdminSidebarWidget {
|
|
|
5935
6887
|
/** Computed collections base path */
|
|
5936
6888
|
collectionsBasePath = computed(() => `${this.basePath()}/collections`, ...(ngDevMode ? [{ debugName: "collectionsBasePath" }] : []));
|
|
5937
6889
|
/** Collections grouped by admin.group field */
|
|
5938
|
-
collectionGroups = computed(() => {
|
|
5939
|
-
const collections = this.collections();
|
|
5940
|
-
const DEFAULT_GROUP = 'Collections';
|
|
5941
|
-
const groupMap = new Map();
|
|
5942
|
-
for (const c of collections) {
|
|
5943
|
-
const name = c.admin?.group ?? DEFAULT_GROUP;
|
|
5944
|
-
const list = groupMap.get(name) ?? [];
|
|
5945
|
-
list.push(c);
|
|
5946
|
-
groupMap.set(name, list);
|
|
5947
|
-
}
|
|
5948
|
-
// Named groups first (in order of first appearance), default last
|
|
5949
|
-
const groups = [];
|
|
5950
|
-
for (const [name, colls] of groupMap) {
|
|
5951
|
-
if (name !== DEFAULT_GROUP)
|
|
5952
|
-
groups.push({ name, collections: colls });
|
|
5953
|
-
}
|
|
5954
|
-
const defaultGroup = groupMap.get(DEFAULT_GROUP);
|
|
5955
|
-
if (defaultGroup)
|
|
5956
|
-
groups.push({ name: DEFAULT_GROUP, collections: defaultGroup });
|
|
5957
|
-
return groups;
|
|
5958
|
-
}, ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
|
|
6890
|
+
collectionGroups = computed(() => groupCollections(this.collections()), ...(ngDevMode ? [{ debugName: "collectionGroups" }] : []));
|
|
5959
6891
|
/** Globals grouped by admin.group field */
|
|
5960
6892
|
globalGroups = computed(() => {
|
|
5961
6893
|
const globals = this.globals();
|
|
@@ -6109,7 +7041,7 @@ class AdminSidebarWidget {
|
|
|
6109
7041
|
/>
|
|
6110
7042
|
|
|
6111
7043
|
<!-- Collection Sections (grouped by admin.group) -->
|
|
6112
|
-
@for (group of collectionGroups(); track group.
|
|
7044
|
+
@for (group of collectionGroups(); track group.id) {
|
|
6113
7045
|
<mcms-sidebar-section [title]="group.name">
|
|
6114
7046
|
@for (collection of group.collections; track collection.slug) {
|
|
6115
7047
|
<mcms-sidebar-nav-item
|
|
@@ -6147,6 +7079,7 @@ class AdminSidebarWidget {
|
|
|
6147
7079
|
[label]="route.label"
|
|
6148
7080
|
[href]="basePath() + '/' + route.path"
|
|
6149
7081
|
[icon]="route.icon"
|
|
7082
|
+
[exact]="true"
|
|
6150
7083
|
/>
|
|
6151
7084
|
}
|
|
6152
7085
|
</mcms-sidebar-section>
|
|
@@ -6280,7 +7213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6280
7213
|
/>
|
|
6281
7214
|
|
|
6282
7215
|
<!-- Collection Sections (grouped by admin.group) -->
|
|
6283
|
-
@for (group of collectionGroups(); track group.
|
|
7216
|
+
@for (group of collectionGroups(); track group.id) {
|
|
6284
7217
|
<mcms-sidebar-section [title]="group.name">
|
|
6285
7218
|
@for (collection of group.collections; track collection.slug) {
|
|
6286
7219
|
<mcms-sidebar-nav-item
|
|
@@ -6318,6 +7251,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6318
7251
|
[label]="route.label"
|
|
6319
7252
|
[href]="basePath() + '/' + route.path"
|
|
6320
7253
|
[icon]="route.icon"
|
|
7254
|
+
[exact]="true"
|
|
6321
7255
|
/>
|
|
6322
7256
|
}
|
|
6323
7257
|
</mcms-sidebar-section>
|
|
@@ -7417,6 +8351,8 @@ class DashboardPage {
|
|
|
7417
8351
|
// Filter to only accessible, non-hidden collections
|
|
7418
8352
|
return all.filter((c) => !c.admin?.hidden && accessible.includes(c.slug));
|
|
7419
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" }] : []));
|
|
7420
8356
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DashboardPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7421
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: `
|
|
7422
8358
|
<header class="mb-10">
|
|
@@ -7424,41 +8360,50 @@ class DashboardPage {
|
|
|
7424
8360
|
<p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
|
|
7425
8361
|
</header>
|
|
7426
8362
|
|
|
7427
|
-
|
|
7428
|
-
<
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
stroke-linecap="round"
|
|
7448
|
-
stroke-linejoin="round"
|
|
7449
|
-
stroke-width="1.5"
|
|
7450
|
-
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"
|
|
7451
|
-
/>
|
|
7452
|
-
</svg>
|
|
7453
|
-
</div>
|
|
7454
|
-
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
7455
|
-
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
7456
|
-
Add collections to your configuration to start managing content.
|
|
7457
|
-
</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>
|
|
7458
8383
|
</div>
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
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
|
+
}
|
|
7462
8407
|
`, isInline: true, dependencies: [{ kind: "component", type: CollectionCardWidget, selector: "mcms-collection-card", inputs: ["collection", "basePath", "showDocumentCount"], outputs: ["viewAll"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7463
8408
|
}
|
|
7464
8409
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DashboardPage, decorators: [{
|
|
@@ -7474,41 +8419,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
7474
8419
|
<p class="text-muted-foreground mt-3 text-lg">Manage your content and collections</p>
|
|
7475
8420
|
</header>
|
|
7476
8421
|
|
|
7477
|
-
|
|
7478
|
-
<
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
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"
|
|
7487
8455
|
>
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
stroke="currentColor"
|
|
7495
|
-
>
|
|
7496
|
-
<path
|
|
7497
|
-
stroke-linecap="round"
|
|
7498
|
-
stroke-linejoin="round"
|
|
7499
|
-
stroke-width="1.5"
|
|
7500
|
-
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"
|
|
7501
|
-
/>
|
|
7502
|
-
</svg>
|
|
7503
|
-
</div>
|
|
7504
|
-
<p class="text-foreground font-medium text-lg">No collections configured</p>
|
|
7505
|
-
<p class="text-sm text-muted-foreground mt-2 text-center max-w-sm">
|
|
7506
|
-
Add collections to your configuration to start managing content.
|
|
7507
|
-
</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
|
+
}
|
|
7508
8462
|
</div>
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
8463
|
+
</section>
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
7512
8466
|
`,
|
|
7513
8467
|
}]
|
|
7514
8468
|
}] });
|
|
@@ -8864,15 +9818,109 @@ var collectionList_page = /*#__PURE__*/Object.freeze({
|
|
|
8864
9818
|
CollectionListPage: CollectionListPage
|
|
8865
9819
|
});
|
|
8866
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
|
+
|
|
8867
9906
|
/**
|
|
8868
9907
|
* Live Preview Widget
|
|
8869
9908
|
*
|
|
8870
9909
|
* Displays an iframe that shows a live preview of the document being edited.
|
|
8871
|
-
*
|
|
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().
|
|
8872
9921
|
*/
|
|
8873
9922
|
class LivePreviewComponent {
|
|
8874
9923
|
document = inject(DOCUMENT);
|
|
8875
|
-
sanitizer = inject(DomSanitizer);
|
|
8876
9924
|
destroyRef = inject(DestroyRef);
|
|
8877
9925
|
/** Preview configuration from collection admin config */
|
|
8878
9926
|
preview = input.required(...(ngDevMode ? [{ debugName: "preview" }] : []));
|
|
@@ -8888,8 +9936,8 @@ class LivePreviewComponent {
|
|
|
8888
9936
|
deviceSize = signal('desktop', ...(ngDevMode ? [{ debugName: "deviceSize" }] : []));
|
|
8889
9937
|
/** Refresh counter to force iframe reload */
|
|
8890
9938
|
refreshCounter = signal(0, ...(ngDevMode ? [{ debugName: "refreshCounter" }] : []));
|
|
8891
|
-
/** Reference to the iframe element */
|
|
8892
|
-
|
|
9939
|
+
/** Reference to the static iframe element (available when previewUrl is non-null) */
|
|
9940
|
+
previewIframe = viewChild('previewIframe', ...(ngDevMode ? [{ debugName: "previewIframe" }] : []));
|
|
8893
9941
|
/** Compute the raw preview URL */
|
|
8894
9942
|
previewUrl = computed(() => {
|
|
8895
9943
|
// Force recomputation on refresh
|
|
@@ -8906,18 +9954,25 @@ class LivePreviewComponent {
|
|
|
8906
9954
|
return null;
|
|
8907
9955
|
}
|
|
8908
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
|
+
}
|
|
8909
9971
|
if (previewConfig === true && id) {
|
|
8910
9972
|
return `/api/${slug}/${id}/preview`;
|
|
8911
9973
|
}
|
|
8912
9974
|
return null;
|
|
8913
9975
|
}, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
|
|
8914
|
-
/** Sanitized preview URL for iframe binding */
|
|
8915
|
-
safePreviewUrl = computed(() => {
|
|
8916
|
-
const url = this.previewUrl();
|
|
8917
|
-
if (!url)
|
|
8918
|
-
return null;
|
|
8919
|
-
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
|
8920
|
-
}, ...(ngDevMode ? [{ debugName: "safePreviewUrl" }] : []));
|
|
8921
9976
|
/** Computed iframe width based on device size */
|
|
8922
9977
|
iframeWidth = computed(() => {
|
|
8923
9978
|
switch (this.deviceSize()) {
|
|
@@ -8929,21 +9984,57 @@ class LivePreviewComponent {
|
|
|
8929
9984
|
return '100%';
|
|
8930
9985
|
}
|
|
8931
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" }] : []));
|
|
8932
9997
|
/** Debounce timer for postMessage updates */
|
|
8933
9998
|
debounceTimer = undefined;
|
|
8934
9999
|
constructor() {
|
|
8935
|
-
//
|
|
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).
|
|
8936
10027
|
effect(() => {
|
|
8937
10028
|
const data = this.documentData();
|
|
8938
|
-
const
|
|
8939
|
-
if (!
|
|
10029
|
+
const iframeRef = this.previewIframe();
|
|
10030
|
+
if (!iframeRef?.nativeElement.contentWindow)
|
|
8940
10031
|
return;
|
|
8941
10032
|
// Debounce to avoid thrashing
|
|
8942
10033
|
if (this.debounceTimer) {
|
|
8943
10034
|
clearTimeout(this.debounceTimer);
|
|
8944
10035
|
}
|
|
8945
10036
|
this.debounceTimer = this.document.defaultView?.setTimeout(() => {
|
|
8946
|
-
const iframeWindow =
|
|
10037
|
+
const iframeWindow = iframeRef.nativeElement.contentWindow;
|
|
8947
10038
|
if (iframeWindow) {
|
|
8948
10039
|
const targetOrigin = this.document.defaultView?.location?.origin ?? '';
|
|
8949
10040
|
iframeWindow.postMessage({ type: 'momentum-preview-update', data }, targetOrigin);
|
|
@@ -8979,7 +10070,7 @@ class LivePreviewComponent {
|
|
|
8979
10070
|
this.refreshCounter.update((c) => c + 1);
|
|
8980
10071
|
}
|
|
8981
10072
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: LivePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8982
|
-
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: `
|
|
8983
10074
|
<!-- Preview toolbar -->
|
|
8984
10075
|
<div class="flex items-center gap-2 px-4 py-2 border-b border-border bg-muted/50">
|
|
8985
10076
|
<span class="text-sm font-medium text-foreground">Preview</span>
|
|
@@ -9047,15 +10138,14 @@ class LivePreviewComponent {
|
|
|
9047
10138
|
|
|
9048
10139
|
<!-- Preview iframe container -->
|
|
9049
10140
|
<div class="flex-1 overflow-auto bg-muted/30 flex justify-center p-4">
|
|
9050
|
-
@if (
|
|
10141
|
+
@if (previewUrl()) {
|
|
10142
|
+
<!-- Static iframe with no dynamic bindings (avoids NG0910).
|
|
10143
|
+
src/sandbox/width are set via nativeElement in an effect(). -->
|
|
9051
10144
|
<iframe
|
|
9052
|
-
#
|
|
9053
|
-
[src]="url"
|
|
9054
|
-
[style.width]="iframeWidth()"
|
|
9055
|
-
class="h-full bg-white border border-border rounded-md shadow-sm transition-[width] duration-300"
|
|
9056
|
-
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
10145
|
+
#previewIframe
|
|
9057
10146
|
title="Live document preview"
|
|
9058
10147
|
data-testid="preview-iframe"
|
|
10148
|
+
class="h-full bg-white border border-border rounded-md shadow-sm transition-[width] duration-300"
|
|
9059
10149
|
></iframe>
|
|
9060
10150
|
} @else {
|
|
9061
10151
|
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
@@ -9133,255 +10223,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
9133
10223
|
(click)="refreshPreview()"
|
|
9134
10224
|
data-testid="preview-refresh"
|
|
9135
10225
|
aria-label="Refresh preview"
|
|
9136
|
-
>
|
|
9137
|
-
↻ Refresh
|
|
9138
|
-
</button>
|
|
9139
|
-
</div>
|
|
9140
|
-
|
|
9141
|
-
<!-- Preview iframe container -->
|
|
9142
|
-
<div class="flex-1 overflow-auto bg-muted/30 flex justify-center p-4">
|
|
9143
|
-
@if (
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
></iframe>
|
|
9153
|
-
} @else {
|
|
9154
|
-
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
9155
|
-
Preview not available
|
|
9156
|
-
</div>
|
|
9157
|
-
}
|
|
9158
|
-
</div>
|
|
9159
|
-
`,
|
|
9160
|
-
}]
|
|
9161
|
-
}], 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 }] }] } });
|
|
9162
|
-
|
|
9163
|
-
/**
|
|
9164
|
-
* Collection View Page Component
|
|
9165
|
-
*
|
|
9166
|
-
* Displays a read-only view of a document using the EntityViewWidget.
|
|
9167
|
-
* When preview is enabled, shows a toggleable live preview panel.
|
|
9168
|
-
*/
|
|
9169
|
-
class CollectionViewPage {
|
|
9170
|
-
route = inject(ActivatedRoute);
|
|
9171
|
-
router = inject(Router);
|
|
9172
|
-
basePath = '/admin/collections';
|
|
9173
|
-
/** Whether the live preview panel is visible */
|
|
9174
|
-
showPreview = signal(true, ...(ngDevMode ? [{ debugName: "showPreview" }] : []));
|
|
9175
|
-
/** Reference to the entity view widget to read its entity data */
|
|
9176
|
-
entityViewRef = viewChild('entityView', ...(ngDevMode ? [{ debugName: "entityViewRef" }] : []));
|
|
9177
|
-
// Reactive slug signal that updates when route params change
|
|
9178
|
-
slug = toSignal(this.route.paramMap.pipe(map((params) => params.get('slug') ?? '')), { initialValue: this.route.snapshot.paramMap.get('slug') ?? '' });
|
|
9179
|
-
// Reactive entity ID signal
|
|
9180
|
-
entityId = toSignal(this.route.paramMap.pipe(map((params) => params.get('id') ?? '')), {
|
|
9181
|
-
initialValue: this.route.snapshot.paramMap.get('id') ?? '',
|
|
9182
|
-
});
|
|
9183
|
-
collection = computed(() => {
|
|
9184
|
-
const currentSlug = this.slug();
|
|
9185
|
-
if (!currentSlug)
|
|
9186
|
-
return undefined;
|
|
9187
|
-
const collections = getCollectionsFromRouteData(this.route.parent?.snapshot.data);
|
|
9188
|
-
return collections.find((c) => c.slug === currentSlug);
|
|
9189
|
-
}, ...(ngDevMode ? [{ debugName: "collection" }] : []));
|
|
9190
|
-
/** Preview config from collection admin settings */
|
|
9191
|
-
previewConfig = computed(() => {
|
|
9192
|
-
const col = this.collection();
|
|
9193
|
-
return col?.admin?.preview || undefined;
|
|
9194
|
-
}, ...(ngDevMode ? [{ debugName: "previewConfig" }] : []));
|
|
9195
|
-
/** Entity data from the entity view widget (for live preview) */
|
|
9196
|
-
viewEntityData = computed(() => {
|
|
9197
|
-
const view = this.entityViewRef();
|
|
9198
|
-
const entity = view?.entity();
|
|
9199
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T extends Entity with index signature
|
|
9200
|
-
return entity ?? {};
|
|
9201
|
-
}, ...(ngDevMode ? [{ debugName: "viewEntityData" }] : []));
|
|
9202
|
-
onEdit(entity) {
|
|
9203
|
-
const col = this.collection();
|
|
9204
|
-
if (col) {
|
|
9205
|
-
this.router.navigate([this.basePath, col.slug, entity.id, 'edit']);
|
|
9206
|
-
}
|
|
9207
|
-
}
|
|
9208
|
-
onDelete(_entity) {
|
|
9209
|
-
// Navigation is handled by EntityViewWidget
|
|
9210
|
-
}
|
|
9211
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9212
|
-
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: `
|
|
9213
|
-
@if (collection(); as col) {
|
|
9214
|
-
@if (entityId(); as id) {
|
|
9215
|
-
@if (previewConfig(); as preview) {
|
|
9216
|
-
@if (showPreview()) {
|
|
9217
|
-
<!-- Split layout: entity view + preview -->
|
|
9218
|
-
<div class="flex gap-0 h-[calc(100vh-64px)]" data-testid="preview-layout">
|
|
9219
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
9220
|
-
<mcms-entity-view
|
|
9221
|
-
#entityView
|
|
9222
|
-
[collection]="col"
|
|
9223
|
-
[entityId]="id"
|
|
9224
|
-
[basePath]="basePath"
|
|
9225
|
-
(edit)="onEdit($event)"
|
|
9226
|
-
(delete_)="onDelete($event)"
|
|
9227
|
-
>
|
|
9228
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9229
|
-
<button
|
|
9230
|
-
mcms-button
|
|
9231
|
-
variant="ghost"
|
|
9232
|
-
size="sm"
|
|
9233
|
-
data-testid="preview-toggle"
|
|
9234
|
-
(click)="showPreview.set(false)"
|
|
9235
|
-
>
|
|
9236
|
-
Hide Preview
|
|
9237
|
-
</button>
|
|
9238
|
-
</div>
|
|
9239
|
-
</mcms-entity-view>
|
|
9240
|
-
</div>
|
|
9241
|
-
<div class="w-[50%] min-w-[400px] max-w-[720px]">
|
|
9242
|
-
<mcms-live-preview
|
|
9243
|
-
[preview]="preview"
|
|
9244
|
-
[documentData]="viewEntityData()"
|
|
9245
|
-
[collectionSlug]="col.slug"
|
|
9246
|
-
[entityId]="id"
|
|
9247
|
-
/>
|
|
9248
|
-
</div>
|
|
9249
|
-
</div>
|
|
9250
|
-
} @else {
|
|
9251
|
-
<!-- Full-width view (preview hidden) -->
|
|
9252
|
-
<mcms-entity-view
|
|
9253
|
-
#entityView
|
|
9254
|
-
[collection]="col"
|
|
9255
|
-
[entityId]="id"
|
|
9256
|
-
[basePath]="basePath"
|
|
9257
|
-
(edit)="onEdit($event)"
|
|
9258
|
-
(delete_)="onDelete($event)"
|
|
9259
|
-
>
|
|
9260
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9261
|
-
<button
|
|
9262
|
-
mcms-button
|
|
9263
|
-
variant="ghost"
|
|
9264
|
-
size="sm"
|
|
9265
|
-
data-testid="preview-toggle"
|
|
9266
|
-
(click)="showPreview.set(true)"
|
|
9267
|
-
>
|
|
9268
|
-
Show Preview
|
|
9269
|
-
</button>
|
|
9270
|
-
</div>
|
|
9271
|
-
</mcms-entity-view>
|
|
9272
|
-
}
|
|
9273
|
-
} @else {
|
|
9274
|
-
<!-- No preview configured -->
|
|
9275
|
-
<mcms-entity-view
|
|
9276
|
-
#entityView
|
|
9277
|
-
[collection]="col"
|
|
9278
|
-
[entityId]="id"
|
|
9279
|
-
[basePath]="basePath"
|
|
9280
|
-
(edit)="onEdit($event)"
|
|
9281
|
-
(delete_)="onDelete($event)"
|
|
9282
|
-
/>
|
|
9283
|
-
}
|
|
9284
|
-
} @else {
|
|
9285
|
-
<div class="p-12 text-center text-muted-foreground">Entity ID not provided</div>
|
|
9286
|
-
}
|
|
9287
|
-
} @else {
|
|
9288
|
-
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9289
|
-
}
|
|
9290
|
-
`, 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 });
|
|
9291
|
-
}
|
|
9292
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CollectionViewPage, decorators: [{
|
|
9293
|
-
type: Component,
|
|
9294
|
-
args: [{
|
|
9295
|
-
selector: 'mcms-collection-view',
|
|
9296
|
-
imports: [EntityViewWidget, LivePreviewComponent, Button],
|
|
9297
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9298
|
-
host: { class: 'block' },
|
|
9299
|
-
template: `
|
|
9300
|
-
@if (collection(); as col) {
|
|
9301
|
-
@if (entityId(); as id) {
|
|
9302
|
-
@if (previewConfig(); as preview) {
|
|
9303
|
-
@if (showPreview()) {
|
|
9304
|
-
<!-- Split layout: entity view + preview -->
|
|
9305
|
-
<div class="flex gap-0 h-[calc(100vh-64px)]" data-testid="preview-layout">
|
|
9306
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
9307
|
-
<mcms-entity-view
|
|
9308
|
-
#entityView
|
|
9309
|
-
[collection]="col"
|
|
9310
|
-
[entityId]="id"
|
|
9311
|
-
[basePath]="basePath"
|
|
9312
|
-
(edit)="onEdit($event)"
|
|
9313
|
-
(delete_)="onDelete($event)"
|
|
9314
|
-
>
|
|
9315
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9316
|
-
<button
|
|
9317
|
-
mcms-button
|
|
9318
|
-
variant="ghost"
|
|
9319
|
-
size="sm"
|
|
9320
|
-
data-testid="preview-toggle"
|
|
9321
|
-
(click)="showPreview.set(false)"
|
|
9322
|
-
>
|
|
9323
|
-
Hide Preview
|
|
9324
|
-
</button>
|
|
9325
|
-
</div>
|
|
9326
|
-
</mcms-entity-view>
|
|
9327
|
-
</div>
|
|
9328
|
-
<div class="w-[50%] min-w-[400px] max-w-[720px]">
|
|
9329
|
-
<mcms-live-preview
|
|
9330
|
-
[preview]="preview"
|
|
9331
|
-
[documentData]="viewEntityData()"
|
|
9332
|
-
[collectionSlug]="col.slug"
|
|
9333
|
-
[entityId]="id"
|
|
9334
|
-
/>
|
|
9335
|
-
</div>
|
|
9336
|
-
</div>
|
|
9337
|
-
} @else {
|
|
9338
|
-
<!-- Full-width view (preview hidden) -->
|
|
9339
|
-
<mcms-entity-view
|
|
9340
|
-
#entityView
|
|
9341
|
-
[collection]="col"
|
|
9342
|
-
[entityId]="id"
|
|
9343
|
-
[basePath]="basePath"
|
|
9344
|
-
(edit)="onEdit($event)"
|
|
9345
|
-
(delete_)="onDelete($event)"
|
|
9346
|
-
>
|
|
9347
|
-
<div entityViewHeaderExtra class="mt-3">
|
|
9348
|
-
<button
|
|
9349
|
-
mcms-button
|
|
9350
|
-
variant="ghost"
|
|
9351
|
-
size="sm"
|
|
9352
|
-
data-testid="preview-toggle"
|
|
9353
|
-
(click)="showPreview.set(true)"
|
|
9354
|
-
>
|
|
9355
|
-
Show Preview
|
|
9356
|
-
</button>
|
|
9357
|
-
</div>
|
|
9358
|
-
</mcms-entity-view>
|
|
9359
|
-
}
|
|
9360
|
-
} @else {
|
|
9361
|
-
<!-- No preview configured -->
|
|
9362
|
-
<mcms-entity-view
|
|
9363
|
-
#entityView
|
|
9364
|
-
[collection]="col"
|
|
9365
|
-
[entityId]="id"
|
|
9366
|
-
[basePath]="basePath"
|
|
9367
|
-
(edit)="onEdit($event)"
|
|
9368
|
-
(delete_)="onDelete($event)"
|
|
9369
|
-
/>
|
|
9370
|
-
}
|
|
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>
|
|
9371
10242
|
} @else {
|
|
9372
|
-
<div class="
|
|
10243
|
+
<div class="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
10244
|
+
Preview not available
|
|
10245
|
+
</div>
|
|
9373
10246
|
}
|
|
9374
|
-
|
|
9375
|
-
<div class="p-12 text-center text-muted-foreground">Collection not found</div>
|
|
9376
|
-
}
|
|
10247
|
+
</div>
|
|
9377
10248
|
`,
|
|
9378
10249
|
}]
|
|
9379
|
-
}], propDecorators: {
|
|
9380
|
-
|
|
9381
|
-
var collectionView_page = /*#__PURE__*/Object.freeze({
|
|
9382
|
-
__proto__: null,
|
|
9383
|
-
CollectionViewPage: CollectionViewPage
|
|
9384
|
-
});
|
|
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 }] }] } });
|
|
9385
10251
|
|
|
9386
10252
|
/**
|
|
9387
10253
|
* Block Edit Dialog
|
|
@@ -10442,140 +11308,6 @@ var setup_page = /*#__PURE__*/Object.freeze({
|
|
|
10442
11308
|
SetupPage: SetupPage
|
|
10443
11309
|
});
|
|
10444
11310
|
|
|
10445
|
-
/**
|
|
10446
|
-
* Media Preview Component
|
|
10447
|
-
*
|
|
10448
|
-
* Displays a preview of media based on its type:
|
|
10449
|
-
* - Images: Thumbnail preview
|
|
10450
|
-
* - Videos: Video icon with optional poster
|
|
10451
|
-
* - Audio: Audio icon
|
|
10452
|
-
* - Documents: Document icon
|
|
10453
|
-
* - Other: Generic file icon
|
|
10454
|
-
*
|
|
10455
|
-
* @example
|
|
10456
|
-
* ```html
|
|
10457
|
-
* <mcms-media-preview
|
|
10458
|
-
* [media]="mediaDocument"
|
|
10459
|
-
* [size]="'md'"
|
|
10460
|
-
* />
|
|
10461
|
-
* ```
|
|
10462
|
-
*/
|
|
10463
|
-
class MediaPreviewComponent {
|
|
10464
|
-
/** Media data to preview */
|
|
10465
|
-
media = input(null, ...(ngDevMode ? [{ debugName: "media" }] : []));
|
|
10466
|
-
/** Size of the preview */
|
|
10467
|
-
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
10468
|
-
/** Custom class override */
|
|
10469
|
-
class = input('', ...(ngDevMode ? [{ debugName: "class" }] : []));
|
|
10470
|
-
/** Whether to show rounded corners */
|
|
10471
|
-
rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : []));
|
|
10472
|
-
/** Host classes */
|
|
10473
|
-
hostClasses = computed(() => {
|
|
10474
|
-
const sizeClass = this.sizeClasses()[this.size()];
|
|
10475
|
-
const roundedClass = this.rounded() ? 'rounded-md overflow-hidden' : '';
|
|
10476
|
-
return `inline-block ${sizeClass} ${roundedClass} ${this.class()}`;
|
|
10477
|
-
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
10478
|
-
/** Size classes map */
|
|
10479
|
-
sizeClasses = computed(() => ({
|
|
10480
|
-
xs: 'h-8 w-8',
|
|
10481
|
-
sm: 'h-12 w-12',
|
|
10482
|
-
md: 'h-20 w-20',
|
|
10483
|
-
lg: 'h-32 w-32',
|
|
10484
|
-
xl: 'h-48 w-48',
|
|
10485
|
-
}), ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
|
|
10486
|
-
/** Icon size classes */
|
|
10487
|
-
iconClasses = computed(() => {
|
|
10488
|
-
const sizes = {
|
|
10489
|
-
xs: 'text-lg',
|
|
10490
|
-
sm: 'text-xl',
|
|
10491
|
-
md: 'text-3xl',
|
|
10492
|
-
lg: 'text-4xl',
|
|
10493
|
-
xl: 'text-6xl',
|
|
10494
|
-
};
|
|
10495
|
-
return `${sizes[this.size()]} text-mcms-muted-foreground`;
|
|
10496
|
-
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
10497
|
-
/** Whether the media is an image */
|
|
10498
|
-
isImage = computed(() => {
|
|
10499
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10500
|
-
return mimeType.startsWith('image/');
|
|
10501
|
-
}, ...(ngDevMode ? [{ debugName: "isImage" }] : []));
|
|
10502
|
-
/** Whether the media is a video */
|
|
10503
|
-
isVideo = computed(() => {
|
|
10504
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10505
|
-
return mimeType.startsWith('video/');
|
|
10506
|
-
}, ...(ngDevMode ? [{ debugName: "isVideo" }] : []));
|
|
10507
|
-
/** Whether the media is audio */
|
|
10508
|
-
isAudio = computed(() => {
|
|
10509
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10510
|
-
return mimeType.startsWith('audio/');
|
|
10511
|
-
}, ...(ngDevMode ? [{ debugName: "isAudio" }] : []));
|
|
10512
|
-
/** Image URL for preview */
|
|
10513
|
-
imageUrl = computed(() => {
|
|
10514
|
-
const media = this.media();
|
|
10515
|
-
if (!media)
|
|
10516
|
-
return '';
|
|
10517
|
-
return media.url ?? `/api/media/file/${media.path}`;
|
|
10518
|
-
}, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
|
|
10519
|
-
/** Icon name based on media type */
|
|
10520
|
-
iconName = computed(() => {
|
|
10521
|
-
const mimeType = this.media()?.mimeType ?? '';
|
|
10522
|
-
if (mimeType.startsWith('video/')) {
|
|
10523
|
-
return heroFilm;
|
|
10524
|
-
}
|
|
10525
|
-
if (mimeType.startsWith('audio/')) {
|
|
10526
|
-
return heroMusicalNote;
|
|
10527
|
-
}
|
|
10528
|
-
if (mimeType === 'application/pdf') {
|
|
10529
|
-
return heroDocumentText;
|
|
10530
|
-
}
|
|
10531
|
-
if (mimeType.startsWith('application/zip') || mimeType.includes('compressed')) {
|
|
10532
|
-
return heroArchiveBox;
|
|
10533
|
-
}
|
|
10534
|
-
if (mimeType.startsWith('image/')) {
|
|
10535
|
-
return heroPhoto;
|
|
10536
|
-
}
|
|
10537
|
-
return heroDocument;
|
|
10538
|
-
}, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
|
|
10539
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10540
|
-
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: `
|
|
10541
|
-
@if (isImage()) {
|
|
10542
|
-
<img
|
|
10543
|
-
[src]="imageUrl()"
|
|
10544
|
-
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
10545
|
-
class="h-full w-full object-cover"
|
|
10546
|
-
/>
|
|
10547
|
-
} @else {
|
|
10548
|
-
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
10549
|
-
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
10550
|
-
</div>
|
|
10551
|
-
}
|
|
10552
|
-
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10553
|
-
}
|
|
10554
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: MediaPreviewComponent, decorators: [{
|
|
10555
|
-
type: Component,
|
|
10556
|
-
args: [{
|
|
10557
|
-
selector: 'mcms-media-preview',
|
|
10558
|
-
host: {
|
|
10559
|
-
'[class]': 'hostClasses()',
|
|
10560
|
-
},
|
|
10561
|
-
template: `
|
|
10562
|
-
@if (isImage()) {
|
|
10563
|
-
<img
|
|
10564
|
-
[src]="imageUrl()"
|
|
10565
|
-
[alt]="media()?.alt ?? media()?.filename ?? 'Media preview'"
|
|
10566
|
-
class="h-full w-full object-cover"
|
|
10567
|
-
/>
|
|
10568
|
-
} @else {
|
|
10569
|
-
<div class="flex h-full w-full items-center justify-center bg-mcms-muted">
|
|
10570
|
-
<ng-icon [name]="iconName()" [class]="iconClasses()" />
|
|
10571
|
-
</div>
|
|
10572
|
-
}
|
|
10573
|
-
`,
|
|
10574
|
-
imports: [NgIcon],
|
|
10575
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10576
|
-
}]
|
|
10577
|
-
}], 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 }] }] } });
|
|
10578
|
-
|
|
10579
11311
|
/**
|
|
10580
11312
|
* Type guard to check if a value has the shape of a MediaEditItem.
|
|
10581
11313
|
*/
|
|
@@ -12603,18 +13335,18 @@ function provideMomentumFieldRenderers() {
|
|
|
12603
13335
|
registry.register('checkbox', () => Promise.resolve().then(function () { return checkboxField_component; }).then((m) => m.CheckboxFieldRenderer));
|
|
12604
13336
|
registry.register('date', () => Promise.resolve().then(function () { return dateField_component; }).then((m) => m.DateFieldRenderer));
|
|
12605
13337
|
registry.register('upload', () => Promise.resolve().then(function () { return uploadField_component; }).then((m) => m.UploadFieldRenderer));
|
|
12606
|
-
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));
|
|
12607
13339
|
// Layout field renderers (support nested field rendering)
|
|
12608
|
-
registry.register('group', () => import('./momentumcms-admin-group-field.component-
|
|
12609
|
-
registry.register('array', () => import('./momentumcms-admin-array-field.component-
|
|
12610
|
-
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));
|
|
12611
13343
|
// Visual block editor variant (blocks field with admin.editor === 'visual')
|
|
12612
13344
|
registry.register('blocks-visual', () => Promise.resolve().then(function () { return visualBlockEditor_component; }).then((m) => m.VisualBlockEditorComponent));
|
|
12613
|
-
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));
|
|
12614
13346
|
// Layout-only renderers (tabs, collapsible, row)
|
|
12615
|
-
registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-
|
|
12616
|
-
registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-
|
|
12617
|
-
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));
|
|
12618
13350
|
};
|
|
12619
13351
|
},
|
|
12620
13352
|
},
|
|
@@ -13456,39 +14188,39 @@ class MediaPickerDialog {
|
|
|
13456
14188
|
this.isLoading.set(true);
|
|
13457
14189
|
try {
|
|
13458
14190
|
const collection = this.api.collection(this.collectionSlug());
|
|
13459
|
-
|
|
13460
|
-
|
|
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
|
|
13461
14198
|
if (search) {
|
|
13462
|
-
|
|
14199
|
+
const lowerSearch = search.toLowerCase();
|
|
14200
|
+
items = items.filter((m) => m.filename.toLowerCase().includes(lowerSearch));
|
|
13463
14201
|
}
|
|
13464
|
-
//
|
|
14202
|
+
// Client-side MIME type filter
|
|
13465
14203
|
const mimeTypes = this.data?.mimeTypes;
|
|
13466
14204
|
if (mimeTypes && mimeTypes.length > 0) {
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
if (prefixes.length > 0 || exactTypes.length > 0) {
|
|
13471
|
-
const orConditions = [];
|
|
13472
|
-
for (const prefix of prefixes) {
|
|
13473
|
-
orConditions.push({ mimeType: { startsWith: prefix } });
|
|
13474
|
-
}
|
|
13475
|
-
for (const type of exactTypes) {
|
|
13476
|
-
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));
|
|
13477
14208
|
}
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
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);
|
|
13482
14223
|
}
|
|
13483
|
-
const result = await collection.find({
|
|
13484
|
-
where: Object.keys(whereClause).length > 0 ? whereClause : undefined,
|
|
13485
|
-
page,
|
|
13486
|
-
limit: this.limit(),
|
|
13487
|
-
sort: '-createdAt',
|
|
13488
|
-
});
|
|
13489
|
-
this.mediaItems.set(toMediaItems(result.docs));
|
|
13490
|
-
this.totalDocs.set(result.totalDocs);
|
|
13491
|
-
this.totalPages.set(result.totalPages);
|
|
13492
14224
|
}
|
|
13493
14225
|
catch (error) {
|
|
13494
14226
|
console.error('Failed to load media:', error);
|
|
@@ -13761,9 +14493,10 @@ function getInputFromEvent(event) {
|
|
|
13761
14493
|
* a FieldTree node's FieldState rather than event-based I/O.
|
|
13762
14494
|
*/
|
|
13763
14495
|
class UploadFieldRenderer {
|
|
13764
|
-
|
|
14496
|
+
fileInputRef = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInputRef" }] : []));
|
|
13765
14497
|
uploadService = inject(UploadService);
|
|
13766
14498
|
dialogService = inject(DialogService);
|
|
14499
|
+
api = injectMomentumAPI();
|
|
13767
14500
|
/** Field definition */
|
|
13768
14501
|
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
13769
14502
|
/** Signal forms FieldTree node for this field */
|
|
@@ -13785,6 +14518,43 @@ class UploadFieldRenderer {
|
|
|
13785
14518
|
uploadingFilename = signal('', ...(ngDevMode ? [{ debugName: "uploadingFilename" }] : []));
|
|
13786
14519
|
uploadError = signal(null, ...(ngDevMode ? [{ debugName: "uploadError" }] : []));
|
|
13787
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
|
+
}
|
|
13788
14558
|
/** Unique field ID */
|
|
13789
14559
|
fieldId = computed(() => `field-${this.path().replace(/\./g, '-')}`, ...(ngDevMode ? [{ debugName: "fieldId" }] : []));
|
|
13790
14560
|
/** Computed label */
|
|
@@ -13814,32 +14584,35 @@ class UploadFieldRenderer {
|
|
|
13814
14584
|
const val = this.currentValue();
|
|
13815
14585
|
return val !== null && val !== undefined && val !== '';
|
|
13816
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" }] : []));
|
|
13817
14595
|
/** Media preview data from value */
|
|
13818
14596
|
mediaPreviewData = computed(() => {
|
|
13819
|
-
const
|
|
13820
|
-
if (!
|
|
14597
|
+
const media = this.effectiveMedia();
|
|
14598
|
+
if (!media)
|
|
13821
14599
|
return null;
|
|
13822
|
-
|
|
13823
|
-
if (typeof val === 'object' && val !== null) {
|
|
14600
|
+
if (typeof media === 'object' && media !== null) {
|
|
13824
14601
|
return {
|
|
13825
|
-
url: getStringProp(
|
|
13826
|
-
path: getStringProp(
|
|
13827
|
-
mimeType: getStringProp(
|
|
13828
|
-
filename: getStringProp(
|
|
13829
|
-
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'),
|
|
13830
14607
|
};
|
|
13831
14608
|
}
|
|
13832
|
-
|
|
13833
|
-
// Return a placeholder
|
|
13834
|
-
return {
|
|
13835
|
-
path: String(val),
|
|
13836
|
-
};
|
|
14609
|
+
return null;
|
|
13837
14610
|
}, ...(ngDevMode ? [{ debugName: "mediaPreviewData" }] : []));
|
|
13838
14611
|
/** Media filename from value */
|
|
13839
14612
|
mediaFilename = computed(() => {
|
|
13840
|
-
const
|
|
13841
|
-
if (typeof
|
|
13842
|
-
return getStringProp(
|
|
14613
|
+
const media = this.effectiveMedia();
|
|
14614
|
+
if (typeof media === 'object' && media !== null) {
|
|
14615
|
+
return getStringProp(media, 'filename') ?? 'Selected media';
|
|
13843
14616
|
}
|
|
13844
14617
|
return 'Selected media';
|
|
13845
14618
|
}, ...(ngDevMode ? [{ debugName: "mediaFilename" }] : []));
|
|
@@ -13940,11 +14713,19 @@ class UploadFieldRenderer {
|
|
|
13940
14713
|
triggerFileInput() {
|
|
13941
14714
|
if (this.isDisabled())
|
|
13942
14715
|
return;
|
|
13943
|
-
const
|
|
13944
|
-
if (
|
|
13945
|
-
|
|
14716
|
+
const ref = this.fileInputRef();
|
|
14717
|
+
if (ref) {
|
|
14718
|
+
ref.nativeElement.click();
|
|
13946
14719
|
}
|
|
13947
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
|
+
}
|
|
13948
14729
|
/**
|
|
13949
14730
|
* Handle file selection from input.
|
|
13950
14731
|
*/
|
|
@@ -13989,7 +14770,8 @@ class UploadFieldRenderer {
|
|
|
13989
14770
|
this.uploadProgress.set(0);
|
|
13990
14771
|
this.uploadingFilename.set(file.name);
|
|
13991
14772
|
this.uploadingFile.set(file);
|
|
13992
|
-
this.
|
|
14773
|
+
const relationTo = this.uploadField().relationTo;
|
|
14774
|
+
this.uploadService.uploadToCollection(relationTo, file).subscribe({
|
|
13993
14775
|
next: (progress) => {
|
|
13994
14776
|
this.uploadProgress.set(progress.progress);
|
|
13995
14777
|
if (progress.status === 'complete' && progress.result) {
|
|
@@ -14047,7 +14829,7 @@ class UploadFieldRenderer {
|
|
|
14047
14829
|
}
|
|
14048
14830
|
}
|
|
14049
14831
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: UploadFieldRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14050
|
-
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: `
|
|
14051
14833
|
<mcms-form-field
|
|
14052
14834
|
[id]="fieldId()"
|
|
14053
14835
|
[required]="required()"
|
|
@@ -14105,7 +14887,7 @@ class UploadFieldRenderer {
|
|
|
14105
14887
|
</div>
|
|
14106
14888
|
</div>
|
|
14107
14889
|
} @else {
|
|
14108
|
-
<!-- Drop zone -->
|
|
14890
|
+
<!-- Drop zone — keyboard-accessible for WCAG 2.1 SC 2.1.1 -->
|
|
14109
14891
|
<div
|
|
14110
14892
|
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
14111
14893
|
[class.border-mcms-border]="!isDragging()"
|
|
@@ -14113,16 +14895,16 @@ class UploadFieldRenderer {
|
|
|
14113
14895
|
[class.bg-mcms-primary/5]="isDragging()"
|
|
14114
14896
|
[class.cursor-pointer]="!isDisabled()"
|
|
14115
14897
|
[class.opacity-50]="isDisabled()"
|
|
14116
|
-
tabindex="0"
|
|
14117
14898
|
role="button"
|
|
14899
|
+
tabindex="0"
|
|
14900
|
+
[attr.aria-label]="'Upload file for ' + label()"
|
|
14118
14901
|
[attr.aria-disabled]="isDisabled()"
|
|
14119
|
-
[attr.aria-label]="'Upload ' + label() + ' file. Drag and drop or click to browse.'"
|
|
14120
14902
|
(dragover)="onDragOver($event)"
|
|
14121
14903
|
(dragleave)="onDragLeave($event)"
|
|
14122
14904
|
(drop)="onDrop($event)"
|
|
14123
14905
|
(click)="triggerFileInput()"
|
|
14124
14906
|
(keydown.enter)="triggerFileInput()"
|
|
14125
|
-
(keydown.space)="
|
|
14907
|
+
(keydown.space)="onDropZoneSpace($event)"
|
|
14126
14908
|
>
|
|
14127
14909
|
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
14128
14910
|
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
@@ -14145,31 +14927,31 @@ class UploadFieldRenderer {
|
|
|
14145
14927
|
</p>
|
|
14146
14928
|
}
|
|
14147
14929
|
</div>
|
|
14148
|
-
@if (!isDisabled()) {
|
|
14149
|
-
<div class="mt-2 flex gap-2">
|
|
14150
|
-
<button
|
|
14151
|
-
mcms-button
|
|
14152
|
-
variant="outline"
|
|
14153
|
-
size="sm"
|
|
14154
|
-
type="button"
|
|
14155
|
-
(click)="$event.stopPropagation(); openMediaPicker()"
|
|
14156
|
-
>
|
|
14157
|
-
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
14158
|
-
Select from library
|
|
14159
|
-
</button>
|
|
14160
|
-
</div>
|
|
14161
|
-
}
|
|
14162
14930
|
</div>
|
|
14163
|
-
<input
|
|
14164
|
-
#fileInput
|
|
14165
|
-
type="file"
|
|
14166
|
-
class="sr-only"
|
|
14167
|
-
[accept]="acceptAttribute()"
|
|
14168
|
-
[disabled]="isDisabled()"
|
|
14169
|
-
(change)="onFileSelected($event)"
|
|
14170
|
-
[attr.aria-label]="'Choose file for ' + label()"
|
|
14171
|
-
/>
|
|
14172
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
|
+
/>
|
|
14173
14955
|
}
|
|
14174
14956
|
|
|
14175
14957
|
@if (uploadError()) {
|
|
@@ -14243,7 +15025,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14243
15025
|
</div>
|
|
14244
15026
|
</div>
|
|
14245
15027
|
} @else {
|
|
14246
|
-
<!-- Drop zone -->
|
|
15028
|
+
<!-- Drop zone — keyboard-accessible for WCAG 2.1 SC 2.1.1 -->
|
|
14247
15029
|
<div
|
|
14248
15030
|
class="relative rounded-lg border-2 border-dashed transition-colors"
|
|
14249
15031
|
[class.border-mcms-border]="!isDragging()"
|
|
@@ -14251,16 +15033,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14251
15033
|
[class.bg-mcms-primary/5]="isDragging()"
|
|
14252
15034
|
[class.cursor-pointer]="!isDisabled()"
|
|
14253
15035
|
[class.opacity-50]="isDisabled()"
|
|
14254
|
-
tabindex="0"
|
|
14255
15036
|
role="button"
|
|
15037
|
+
tabindex="0"
|
|
15038
|
+
[attr.aria-label]="'Upload file for ' + label()"
|
|
14256
15039
|
[attr.aria-disabled]="isDisabled()"
|
|
14257
|
-
[attr.aria-label]="'Upload ' + label() + ' file. Drag and drop or click to browse.'"
|
|
14258
15040
|
(dragover)="onDragOver($event)"
|
|
14259
15041
|
(dragleave)="onDragLeave($event)"
|
|
14260
15042
|
(drop)="onDrop($event)"
|
|
14261
15043
|
(click)="triggerFileInput()"
|
|
14262
15044
|
(keydown.enter)="triggerFileInput()"
|
|
14263
|
-
(keydown.space)="
|
|
15045
|
+
(keydown.space)="onDropZoneSpace($event)"
|
|
14264
15046
|
>
|
|
14265
15047
|
<div class="flex flex-col items-center justify-center gap-2 p-8">
|
|
14266
15048
|
<ng-icon [name]="uploadIcon" class="h-10 w-10 text-mcms-muted-foreground" aria-hidden="true" />
|
|
@@ -14283,31 +15065,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14283
15065
|
</p>
|
|
14284
15066
|
}
|
|
14285
15067
|
</div>
|
|
14286
|
-
@if (!isDisabled()) {
|
|
14287
|
-
<div class="mt-2 flex gap-2">
|
|
14288
|
-
<button
|
|
14289
|
-
mcms-button
|
|
14290
|
-
variant="outline"
|
|
14291
|
-
size="sm"
|
|
14292
|
-
type="button"
|
|
14293
|
-
(click)="$event.stopPropagation(); openMediaPicker()"
|
|
14294
|
-
>
|
|
14295
|
-
<ng-icon [name]="photoIcon" class="h-4 w-4" />
|
|
14296
|
-
Select from library
|
|
14297
|
-
</button>
|
|
14298
|
-
</div>
|
|
14299
|
-
}
|
|
14300
15068
|
</div>
|
|
14301
|
-
<input
|
|
14302
|
-
#fileInput
|
|
14303
|
-
type="file"
|
|
14304
|
-
class="sr-only"
|
|
14305
|
-
[accept]="acceptAttribute()"
|
|
14306
|
-
[disabled]="isDisabled()"
|
|
14307
|
-
(change)="onFileSelected($event)"
|
|
14308
|
-
[attr.aria-label]="'Choose file for ' + label()"
|
|
14309
|
-
/>
|
|
14310
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
|
+
/>
|
|
14311
15093
|
}
|
|
14312
15094
|
|
|
14313
15095
|
@if (uploadError()) {
|
|
@@ -14316,7 +15098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
14316
15098
|
</mcms-form-field>
|
|
14317
15099
|
`,
|
|
14318
15100
|
}]
|
|
14319
|
-
}], 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 }] }] } });
|
|
14320
15102
|
|
|
14321
15103
|
var uploadField_component = /*#__PURE__*/Object.freeze({
|
|
14322
15104
|
__proto__: null,
|
|
@@ -14330,4 +15112,4 @@ var uploadField_component = /*#__PURE__*/Object.freeze({
|
|
|
14330
15112
|
*/
|
|
14331
15113
|
|
|
14332
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 };
|
|
14333
|
-
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-
|
|
15115
|
+
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-D_47TVaR.mjs.map
|