@masterteam/document-library 0.0.1

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.
@@ -0,0 +1,1865 @@
1
+ import * as i1$1 from '@angular/common';
2
+ import { CommonModule, DatePipe } from '@angular/common';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+ import * as i0 from '@angular/core';
5
+ import { inject, Injectable, input, signal, effect, Component, computed, output, viewChild, untracked } from '@angular/core';
6
+ import * as i1 from '@angular/forms';
7
+ import { FormsModule } from '@angular/forms';
8
+ import { map, filter, isObservable, firstValueFrom } from 'rxjs';
9
+ import { Breadcrumb } from '@masterteam/components/breadcrumb';
10
+ import { Avatar } from '@masterteam/components/avatar';
11
+ import { Button } from '@masterteam/components/button';
12
+ import { Card } from '@masterteam/components/card';
13
+ import { ConfirmationService } from '@masterteam/components/confirmation';
14
+ import { ModalService } from '@masterteam/components/modal';
15
+ import { Table } from '@masterteam/components/table';
16
+ import { TextField } from '@masterteam/components/text-field';
17
+ import { ToastService } from '@masterteam/components/toast';
18
+ import { Icon } from '@masterteam/icons';
19
+ import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
20
+ import { Popover } from 'primeng/popover';
21
+ import { HttpClient, HttpContext, HttpEventType, HttpHeaders } from '@angular/common/http';
22
+ import { REQUEST_CONTEXT } from '@masterteam/components';
23
+ import { ModalRef } from '@masterteam/components/dialog';
24
+ import { ToggleField } from '@masterteam/components/toggle-field';
25
+ import { UploadField } from '@masterteam/components/upload-field';
26
+
27
+ function normalizeDocumentLibraryLabel(label) {
28
+ if (!label) {
29
+ return { display: '' };
30
+ }
31
+ return {
32
+ display: label.display ?? label.en ?? label.ar ?? '',
33
+ en: label.en ?? label.display ?? label.ar ?? '',
34
+ ar: label.ar ?? label.display ?? label.en ?? '',
35
+ };
36
+ }
37
+ function getDocumentLibraryFolderLabel(folder, lang = 'en') {
38
+ const label = folder.label ?? {};
39
+ return label[lang] ?? label.display ?? label.en ?? label.ar ?? '';
40
+ }
41
+ function toDocumentLibraryFolderEntry(folder) {
42
+ return {
43
+ ...folder,
44
+ isFolder: true,
45
+ };
46
+ }
47
+ function normalizeDocumentLibraryFolder(folder) {
48
+ return {
49
+ ...folder,
50
+ label: normalizeDocumentLibraryLabel(folder.label),
51
+ isEditableFolder: folder.isEditableFolder ?? false,
52
+ isNameEditable: folder.isNameEditable ?? false,
53
+ isPrimaryFolder: folder.isPrimaryFolder ?? false,
54
+ };
55
+ }
56
+ function normalizeDocumentLibraryFile(file) {
57
+ return {
58
+ ...file,
59
+ created: file.created || new Date().toISOString(),
60
+ isFolder: false,
61
+ };
62
+ }
63
+ function normalizeDocumentLibraryEntry(entry) {
64
+ if (entry.isFolder) {
65
+ return toDocumentLibraryFolderEntry(normalizeDocumentLibraryFolder(entry));
66
+ }
67
+ return normalizeDocumentLibraryFile(entry);
68
+ }
69
+ function getDocumentLibraryEntryTitle(entry, lang = 'en') {
70
+ if (entry.isFolder) {
71
+ return getDocumentLibraryFolderLabel(entry, lang);
72
+ }
73
+ return entry.fileName;
74
+ }
75
+ function getDocumentLibraryFileExtension(fileName) {
76
+ const extension = fileName.split('.').pop();
77
+ return (extension ?? 'file').toLowerCase();
78
+ }
79
+ function formatDocumentLibraryFileSize(size) {
80
+ if (!size) {
81
+ return '0 B';
82
+ }
83
+ const units = ['B', 'KB', 'MB', 'GB'];
84
+ let value = size;
85
+ let unitIndex = 0;
86
+ while (value >= 1024 && unitIndex < units.length - 1) {
87
+ value /= 1024;
88
+ unitIndex += 1;
89
+ }
90
+ return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
91
+ }
92
+
93
+ class DocumentLibraryApiService {
94
+ http = inject(HttpClient);
95
+ defaultContext = new HttpContext().set(REQUEST_CONTEXT, {
96
+ useBaseUrl: false,
97
+ });
98
+ loadRoots(levelId, levelDataId, context) {
99
+ const endpoint = levelDataId == null
100
+ ? `levels/${levelId}/document-library/default`
101
+ : `levels/${levelId}/${levelDataId}/document-library`;
102
+ return this.http
103
+ .get(endpoint, {
104
+ context: this.resolveContext(context),
105
+ })
106
+ .pipe(map((response) => this.unwrap(response) ?? []));
107
+ }
108
+ loadFolderEntries(levelId, folderId, context) {
109
+ return this.http
110
+ .get(`levels/${levelId}/document-library/folders/${folderId}/documents`, {
111
+ context: this.resolveContext(context),
112
+ })
113
+ .pipe(map((response) => this.unwrap(response) ?? []), map((entries) => entries.map((entry) => this.mapEntry(entry))));
114
+ }
115
+ createFolder(levelId, levelDataId, request, context) {
116
+ const endpoint = levelDataId == null
117
+ ? `levels/${levelId}/document-library/folders`
118
+ : `levels/${levelId}/${levelDataId}/document-library/folders`;
119
+ return this.http
120
+ .post(endpoint, request, {
121
+ context: this.resolveContext(context),
122
+ headers: this.mutationHeaders('document-library-create-folder'),
123
+ })
124
+ .pipe(map((response) => this.unwrap(response)));
125
+ }
126
+ addDocument(levelId, levelDataId, request, context) {
127
+ return this.http
128
+ .post(`levels/${levelId}/${levelDataId}/document-library/folders/${request.folderId}/documents`, {
129
+ documentGuid: request.documentGuid,
130
+ }, {
131
+ context: this.resolveContext(context),
132
+ headers: this.mutationHeaders('document-library-add-document'),
133
+ })
134
+ .pipe(map((response) => this.mapFile(this.unwrap(response))));
135
+ }
136
+ uploadAsset(file, endPoint, context) {
137
+ const formData = new FormData();
138
+ formData.append('file', file);
139
+ return this.http
140
+ .post(endPoint, formData, {
141
+ reportProgress: true,
142
+ observe: 'events',
143
+ context: this.resolveContext(context),
144
+ })
145
+ .pipe(map((event) => {
146
+ if (event.type === HttpEventType.UploadProgress) {
147
+ return {
148
+ progress: event.total
149
+ ? Math.round((100 * event.loaded) / event.total)
150
+ : 0,
151
+ completed: false,
152
+ };
153
+ }
154
+ if (event.type === HttpEventType.Response && event.body) {
155
+ const asset = this.mapUploadedAsset(this.unwrap(event.body), file);
156
+ return {
157
+ progress: 100,
158
+ completed: true,
159
+ asset,
160
+ };
161
+ }
162
+ return null;
163
+ }), filter((event) => event !== null));
164
+ }
165
+ renameFolder(levelId, levelDataId, request, context) {
166
+ return this.http
167
+ .put(`levels/${levelId}/${levelDataId}/document-library/folders/${request.folderId}`, {
168
+ folderName: request.folderName,
169
+ }, {
170
+ context: this.resolveContext(context),
171
+ headers: this.mutationHeaders('document-library-rename-folder'),
172
+ })
173
+ .pipe(map((response) => this.unwrap(response)));
174
+ }
175
+ deleteFolder(levelId, levelDataId, request, context) {
176
+ return this.http
177
+ .delete(`levels/${levelId}/${levelDataId}/document-library/folders/${request.folderId}`, {
178
+ context: this.resolveContext(context),
179
+ headers: this.mutationHeaders('document-library-delete-folder'),
180
+ })
181
+ .pipe(map((response) => this.unwrap(response)));
182
+ }
183
+ deleteDocument(levelId, levelDataId, request, context) {
184
+ return this.http
185
+ .delete(`levels/${levelId}/${levelDataId}/document-library/documents/${request.fileId}`, {
186
+ context: this.resolveContext(context),
187
+ headers: this.mutationHeaders('document-library-delete-file'),
188
+ })
189
+ .pipe(map((response) => this.unwrap(response)));
190
+ }
191
+ downloadFile(request, context) {
192
+ const endpoint = this.resolveDownloadEndpoint(request.file.path, request.file.id);
193
+ return this.http
194
+ .get(endpoint, {
195
+ responseType: 'blob',
196
+ context: this.resolveContext(context),
197
+ })
198
+ .pipe(map((blob) => {
199
+ const objectUrl = window.URL.createObjectURL(blob);
200
+ const link = document.createElement('a');
201
+ link.href = objectUrl;
202
+ link.download = request.file.fileName;
203
+ link.click();
204
+ window.URL.revokeObjectURL(objectUrl);
205
+ }));
206
+ }
207
+ unwrap(response) {
208
+ if (response.code === 2) {
209
+ throw new Error(response.errors?.message ||
210
+ response.message ||
211
+ 'Document library request failed.');
212
+ }
213
+ return response.data;
214
+ }
215
+ resolveContext(context) {
216
+ return context ?? this.defaultContext;
217
+ }
218
+ mutationHeaders(seed) {
219
+ return new HttpHeaders({
220
+ 'X-Idempotency-Key': this.idempotencyKey(seed),
221
+ });
222
+ }
223
+ idempotencyKey(seed) {
224
+ if (typeof crypto !== 'undefined' &&
225
+ typeof crypto.randomUUID === 'function') {
226
+ return `${seed}-${crypto.randomUUID()}`;
227
+ }
228
+ return `${seed}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
229
+ }
230
+ mapEntry(entry) {
231
+ return entry.isFolder
232
+ ? entry
233
+ : {
234
+ ...this.mapFile(entry),
235
+ isFolder: false,
236
+ };
237
+ }
238
+ mapFile(file) {
239
+ return {
240
+ id: file.name ?? file.id ?? '',
241
+ name: file.name ?? file.id ?? null,
242
+ fileName: file.fileName,
243
+ contentType: file.contentType,
244
+ size: file.size,
245
+ createdBy: file.createdBy ?? null,
246
+ created: file.created,
247
+ path: this.normalizeDownloadPath(file.path),
248
+ };
249
+ }
250
+ mapUploadedAsset(asset, file) {
251
+ return {
252
+ id: asset.id ?? asset.name ?? file.name,
253
+ name: asset.name ?? asset.id ?? file.name,
254
+ fileName: asset.fileName ?? file.name,
255
+ contentType: asset.contentType ?? file.type,
256
+ size: asset.size ?? file.size,
257
+ };
258
+ }
259
+ resolveDownloadEndpoint(path, fileId) {
260
+ return this.normalizeDownloadPath(path) ?? `uploader/${fileId}`;
261
+ }
262
+ normalizeDownloadPath(path) {
263
+ if (!path) {
264
+ return null;
265
+ }
266
+ const trimmed = path.trim();
267
+ if (!trimmed) {
268
+ return null;
269
+ }
270
+ if (/^https?:\/\//i.test(trimmed)) {
271
+ return trimmed;
272
+ }
273
+ if (/^\/?api\/uploader\//i.test(trimmed)) {
274
+ return trimmed.replace(/^\/?api\//i, '');
275
+ }
276
+ return trimmed.replace(/^\//, '');
277
+ }
278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
279
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryApiService, providedIn: 'root' });
280
+ }
281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryApiService, decorators: [{
282
+ type: Injectable,
283
+ args: [{ providedIn: 'root' }]
284
+ }] });
285
+
286
+ class DocumentLibraryFolderDialog {
287
+ mode = input('create', ...(ngDevMode ? [{ debugName: "mode" }] : []));
288
+ contextLabel = input('', ...(ngDevMode ? [{ debugName: "contextLabel" }] : []));
289
+ initialName = input('', ...(ngDevMode ? [{ debugName: "initialName" }] : []));
290
+ modal = inject(ModalService);
291
+ ref = inject(ModalRef);
292
+ folderName = signal('', ...(ngDevMode ? [{ debugName: "folderName" }] : []));
293
+ constructor() {
294
+ effect(() => {
295
+ this.folderName.set(this.initialName());
296
+ });
297
+ }
298
+ submit() {
299
+ const folderName = this.folderName().trim();
300
+ if (!folderName) {
301
+ return;
302
+ }
303
+ this.ref.close({
304
+ folderName,
305
+ });
306
+ }
307
+ cancel() {
308
+ this.ref.close(null);
309
+ }
310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryFolderDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
311
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: DocumentLibraryFolderDialog, isStandalone: true, selector: "mt-document-library-folder-dialog", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, contextLabel: { classPropertyName: "contextLabel", publicName: "contextLabel", isSignal: true, isRequired: false, transformFunction: null }, initialName: { classPropertyName: "initialName", publicName: "initialName", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'document-library.modals.folder'\">\r\n <div [class]=\"[modal.contentClass, 'flex', 'flex-col', 'gap-4', 'p-4']\">\r\n @if (mode() === \"create\") {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"parent\") }}:\r\n <span class=\"font-medium text-color\">{{ contextLabel() }}</span>\r\n </div>\r\n } @else {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"renameDescription\") }}\r\n </div>\r\n }\r\n\r\n <mt-text-field\r\n [ngModel]=\"folderName()\"\r\n (ngModelChange)=\"folderName.set($event)\"\r\n [label]=\"t('folderName')\"\r\n [placeholder]=\"\r\n mode() === 'create'\r\n ? t('placeholders.create')\r\n : t('placeholders.rename')\r\n \"\r\n />\r\n </div>\r\n\r\n <div\r\n [class]=\"[\r\n modal.footerClass,\r\n 'flex',\r\n 'items-center',\r\n 'justify-end',\r\n 'gap-2',\r\n ]\"\r\n >\r\n <mt-button [label]=\"t('cancel')\" variant=\"outlined\" (onClick)=\"cancel()\" />\r\n\r\n <mt-button\r\n [label]=\"mode() === 'create' ? t('create') : t('save')\"\r\n [icon]=\"mode() === 'create' ? 'file.folder-plus' : 'general.edit-02'\"\r\n [disabled]=\"!folderName().trim()\"\r\n (onClick)=\"submit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] });
312
+ }
313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryFolderDialog, decorators: [{
314
+ type: Component,
315
+ args: [{ selector: 'mt-document-library-folder-dialog', standalone: true, imports: [CommonModule, FormsModule, Button, TextField, TranslocoDirective], template: "<ng-container *transloco=\"let t; prefix: 'document-library.modals.folder'\">\r\n <div [class]=\"[modal.contentClass, 'flex', 'flex-col', 'gap-4', 'p-4']\">\r\n @if (mode() === \"create\") {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"parent\") }}:\r\n <span class=\"font-medium text-color\">{{ contextLabel() }}</span>\r\n </div>\r\n } @else {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"renameDescription\") }}\r\n </div>\r\n }\r\n\r\n <mt-text-field\r\n [ngModel]=\"folderName()\"\r\n (ngModelChange)=\"folderName.set($event)\"\r\n [label]=\"t('folderName')\"\r\n [placeholder]=\"\r\n mode() === 'create'\r\n ? t('placeholders.create')\r\n : t('placeholders.rename')\r\n \"\r\n />\r\n </div>\r\n\r\n <div\r\n [class]=\"[\r\n modal.footerClass,\r\n 'flex',\r\n 'items-center',\r\n 'justify-end',\r\n 'gap-2',\r\n ]\"\r\n >\r\n <mt-button [label]=\"t('cancel')\" variant=\"outlined\" (onClick)=\"cancel()\" />\r\n\r\n <mt-button\r\n [label]=\"mode() === 'create' ? t('create') : t('save')\"\r\n [icon]=\"mode() === 'create' ? 'file.folder-plus' : 'general.edit-02'\"\r\n [disabled]=\"!folderName().trim()\"\r\n (onClick)=\"submit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
316
+ }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], contextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextLabel", required: false }] }], initialName: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialName", required: false }] }] } });
317
+
318
+ class DocumentLibraryUploadDialog {
319
+ mode = input('file', ...(ngDevMode ? [{ debugName: "mode" }] : []));
320
+ folderLabel = input('', ...(ngDevMode ? [{ debugName: "folderLabel" }] : []));
321
+ uploadEndPoint = input('uploader', ...(ngDevMode ? [{ debugName: "uploadEndPoint" }] : []));
322
+ requestContext = input(undefined, ...(ngDevMode ? [{ debugName: "requestContext" }] : []));
323
+ modal = inject(ModalService);
324
+ api = inject(DocumentLibraryApiService);
325
+ ref = inject(ModalRef);
326
+ transloco = inject(TranslocoService);
327
+ uploadedFile = signal(null, ...(ngDevMode ? [{ debugName: "uploadedFile" }] : []));
328
+ folderItems = signal([], ...(ngDevMode ? [{ debugName: "folderItems" }] : []));
329
+ targetRootFolderName = signal('', ...(ngDevMode ? [{ debugName: "targetRootFolderName" }] : []));
330
+ submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
331
+ activeLanguage = toSignal(this.transloco.langChanges$, {
332
+ initialValue: this.transloco.getActiveLang() || 'en',
333
+ });
334
+ selectedFolderName = computed(() => {
335
+ const firstItem = this.folderItems()[0];
336
+ return firstItem ? this.getRelativePathSegments(firstItem.relativePath)[0] ?? '' : '';
337
+ }, ...(ngDevMode ? [{ debugName: "selectedFolderName" }] : []));
338
+ hasTargetRootFolderName = computed(() => !!this.targetRootFolderName().trim(), ...(ngDevMode ? [{ debugName: "hasTargetRootFolderName" }] : []));
339
+ selectedCount = computed(() => this.folderItems().filter((item) => item.enabled).length, ...(ngDevMode ? [{ debugName: "selectedCount" }] : []));
340
+ uploadedCount = computed(() => this.folderItems().filter((item) => item.enabled && item.uploadedAsset)
341
+ .length, ...(ngDevMode ? [{ debugName: "uploadedCount" }] : []));
342
+ pendingCount = computed(() => this.folderItems().filter((item) => item.enabled && !item.uploadedAsset && !item.uploading).length, ...(ngDevMode ? [{ debugName: "pendingCount" }] : []));
343
+ failedCount = computed(() => this.folderItems().filter((item) => item.enabled && !item.uploadedAsset && !!item.error).length, ...(ngDevMode ? [{ debugName: "failedCount" }] : []));
344
+ canCloseWithUploads = computed(() => this.uploadedCount() > 0 && this.pendingCount() === 0, ...(ngDevMode ? [{ debugName: "canCloseWithUploads" }] : []));
345
+ primaryActionLabel = computed(() => {
346
+ if (this.mode() === 'file') {
347
+ return this.translate('primary.attachFile');
348
+ }
349
+ return this.canCloseWithUploads()
350
+ ? this.translate('primary.attachUploadedFiles')
351
+ : this.translate('primary.uploadSelectedFiles');
352
+ }, ...(ngDevMode ? [{ debugName: "primaryActionLabel" }] : []));
353
+ submit() {
354
+ if (this.mode() === 'file') {
355
+ const documentGuid = this.uploadedFile()?.id?.trim() || this.uploadedFile()?.name?.trim();
356
+ if (!documentGuid) {
357
+ return;
358
+ }
359
+ this.ref.close({
360
+ uploads: [
361
+ {
362
+ documentGuid,
363
+ fileName: this.uploadedFile()?.fileName?.trim() ||
364
+ this.uploadedFile()?.name?.trim() ||
365
+ documentGuid,
366
+ relativePath: this.uploadedFile()?.fileName?.trim() ||
367
+ this.uploadedFile()?.name?.trim() ||
368
+ documentGuid,
369
+ },
370
+ ],
371
+ });
372
+ return;
373
+ }
374
+ if (this.canCloseWithUploads()) {
375
+ this.closeWithFolderUploads();
376
+ return;
377
+ }
378
+ void this.uploadSelectedFolderItems();
379
+ }
380
+ cancel() {
381
+ if (this.submitting()) {
382
+ return;
383
+ }
384
+ this.ref.close(null);
385
+ }
386
+ openFolderPicker(folderInput) {
387
+ if (this.submitting()) {
388
+ return;
389
+ }
390
+ folderInput.value = '';
391
+ folderInput.click();
392
+ }
393
+ onFolderSelect(event) {
394
+ const input = event.target;
395
+ const files = Array.from(input?.files ?? []);
396
+ const nextItems = files
397
+ .map((file, index) => this.createFolderUploadItem(file, index))
398
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
399
+ this.folderItems.set(nextItems);
400
+ this.targetRootFolderName.set(nextItems[0]
401
+ ? this.getRelativePathSegments(nextItems[0].relativePath)[0] ?? ''
402
+ : '');
403
+ }
404
+ toggleFolderItem(itemId, enabled) {
405
+ this.folderItems.update((items) => items.map((item) => item.id === itemId
406
+ ? {
407
+ ...item,
408
+ enabled: enabled !== false,
409
+ }
410
+ : item));
411
+ }
412
+ setAllFolderItems(enabled) {
413
+ this.folderItems.update((items) => items.map((item) => ({
414
+ ...item,
415
+ enabled,
416
+ })));
417
+ }
418
+ getFolderItemIcon(item) {
419
+ const extension = getDocumentLibraryFileExtension(item.file.name);
420
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(extension)) {
421
+ return 'image.image-03';
422
+ }
423
+ if (['ts', 'js', 'json', 'html', 'css', 'scss'].includes(extension)) {
424
+ return 'dev.file-code-02';
425
+ }
426
+ if (item.file.type.toLowerCase().includes('pdf')) {
427
+ return 'file.file-06';
428
+ }
429
+ return 'file.file-01';
430
+ }
431
+ getFolderItemStatus(item) {
432
+ if (item.uploading) {
433
+ return this.translate('status.uploading', {
434
+ progress: item.progress,
435
+ });
436
+ }
437
+ if (item.uploadedAsset) {
438
+ return this.translate('status.uploaded');
439
+ }
440
+ if (item.error) {
441
+ return item.error;
442
+ }
443
+ if (!item.enabled) {
444
+ return this.translate('status.excluded');
445
+ }
446
+ return this.translate('status.waiting');
447
+ }
448
+ getFolderItemStatusClass(item) {
449
+ if (item.uploadedAsset) {
450
+ return 'text-emerald-600';
451
+ }
452
+ if (item.error) {
453
+ return 'text-red-600';
454
+ }
455
+ if (!item.enabled) {
456
+ return 'text-slate-400';
457
+ }
458
+ return 'text-muted-color';
459
+ }
460
+ formatFileSize(size) {
461
+ return formatDocumentLibraryFileSize(size);
462
+ }
463
+ createFolderUploadItem(file, index) {
464
+ const relativePath = file.webkitRelativePath || file.name;
465
+ const pathSegments = relativePath.split('/');
466
+ const pathLabel = pathSegments.slice(1).join('/') || file.name;
467
+ return {
468
+ id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
469
+ ? crypto.randomUUID()
470
+ : `${index}-${relativePath}`,
471
+ file,
472
+ relativePath,
473
+ pathLabel,
474
+ enabled: true,
475
+ progress: 0,
476
+ uploading: false,
477
+ uploadedAsset: null,
478
+ error: null,
479
+ };
480
+ }
481
+ async uploadSelectedFolderItems() {
482
+ const queuedItems = this.folderItems().filter((item) => item.enabled && !item.uploadedAsset);
483
+ if (!queuedItems.length) {
484
+ return;
485
+ }
486
+ this.submitting.set(true);
487
+ try {
488
+ for (const item of queuedItems) {
489
+ await this.uploadFolderItem(item.id);
490
+ }
491
+ if (this.failedCount() === 0 && this.canCloseWithUploads()) {
492
+ this.closeWithFolderUploads();
493
+ }
494
+ }
495
+ finally {
496
+ this.submitting.set(false);
497
+ }
498
+ }
499
+ uploadFolderItem(itemId) {
500
+ const item = this.folderItems().find((candidate) => candidate.id === itemId);
501
+ if (!item) {
502
+ return Promise.resolve();
503
+ }
504
+ this.patchFolderItem(itemId, {
505
+ uploading: true,
506
+ progress: 10,
507
+ error: null,
508
+ });
509
+ return new Promise((resolve) => {
510
+ this.api
511
+ .uploadAsset(item.file, this.uploadEndPoint(), this.requestContext())
512
+ .subscribe({
513
+ next: (event) => {
514
+ if (event.completed && event.asset) {
515
+ this.patchFolderItem(itemId, {
516
+ uploading: false,
517
+ progress: 100,
518
+ uploadedAsset: event.asset,
519
+ error: null,
520
+ });
521
+ return;
522
+ }
523
+ this.patchFolderItem(itemId, {
524
+ progress: Math.max(event.progress, 10),
525
+ });
526
+ },
527
+ error: (error) => {
528
+ this.patchFolderItem(itemId, {
529
+ uploading: false,
530
+ progress: 0,
531
+ error: this.getErrorMessage(error),
532
+ });
533
+ resolve();
534
+ },
535
+ complete: () => {
536
+ this.patchFolderItem(itemId, {
537
+ uploading: false,
538
+ });
539
+ resolve();
540
+ },
541
+ });
542
+ });
543
+ }
544
+ patchFolderItem(itemId, patch) {
545
+ this.folderItems.update((items) => items.map((item) => item.id === itemId
546
+ ? {
547
+ ...item,
548
+ ...patch,
549
+ }
550
+ : item));
551
+ }
552
+ closeWithFolderUploads() {
553
+ const uploads = this.folderItems()
554
+ .filter((item) => item.enabled && item.uploadedAsset)
555
+ .map((item) => ({
556
+ documentGuid: item.uploadedAsset?.id?.trim() ||
557
+ item.uploadedAsset?.name?.trim() ||
558
+ '',
559
+ fileName: item.uploadedAsset?.fileName?.trim() || item.file.name,
560
+ relativePath: this.applyTargetRootFolderName(item.relativePath),
561
+ }))
562
+ .filter((item) => !!item.documentGuid);
563
+ if (!uploads.length) {
564
+ return;
565
+ }
566
+ this.ref.close({
567
+ uploads,
568
+ });
569
+ }
570
+ getErrorMessage(error) {
571
+ if (error instanceof Error && error.message) {
572
+ return error.message;
573
+ }
574
+ return this.translate('status.uploadFailed');
575
+ }
576
+ applyTargetRootFolderName(relativePath) {
577
+ const targetRootFolderName = this.targetRootFolderName().trim();
578
+ if (!targetRootFolderName) {
579
+ return relativePath;
580
+ }
581
+ const segments = this.getRelativePathSegments(relativePath);
582
+ if (!segments.length) {
583
+ return relativePath;
584
+ }
585
+ if (segments.length === 1) {
586
+ return [targetRootFolderName, segments[0]].join('/');
587
+ }
588
+ segments[0] = targetRootFolderName;
589
+ return segments.join('/');
590
+ }
591
+ getRelativePathSegments(relativePath) {
592
+ return relativePath
593
+ .split(/[\\/]/)
594
+ .map((segment) => segment.trim())
595
+ .filter(Boolean);
596
+ }
597
+ translate(key, params) {
598
+ this.activeLanguage();
599
+ return this.transloco.translate(`document-library.modals.upload.${key}`, params);
600
+ }
601
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryUploadDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
602
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: DocumentLibraryUploadDialog, isStandalone: true, selector: "mt-document-library-upload-dialog", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, folderLabel: { classPropertyName: "folderLabel", publicName: "folderLabel", isSignal: true, isRequired: false, transformFunction: null }, uploadEndPoint: { classPropertyName: "uploadEndPoint", publicName: "uploadEndPoint", isSignal: true, isRequired: false, transformFunction: null }, requestContext: { classPropertyName: "requestContext", publicName: "requestContext", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'document-library.modals.upload'\">\r\n <div [class]=\"[modal.contentClass, 'flex', 'flex-col', 'gap-4', 'p-4']\">\r\n @if (mode() === \"file\") {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"fileIntro\") }}\r\n <span class=\"font-medium text-color\">{{ folderLabel() }}</span\r\n >.\r\n </div>\r\n\r\n <mt-upload-field\r\n [ngModel]=\"uploadedFile()\"\r\n (ngModelChange)=\"uploadedFile.set($event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n [shape]=\"'card'\"\r\n [title]=\"t('uploadFileTitle')\"\r\n [description]=\"t('uploadFileDescription')\"\r\n [endPoint]=\"uploadEndPoint()\"\r\n [context]=\"requestContext()\"\r\n />\r\n } @else {\r\n <input\r\n #folderInput\r\n type=\"file\"\r\n directory\r\n webkitdirectory\r\n multiple\r\n class=\"hidden\"\r\n (change)=\"onFolderSelect($event)\"\r\n />\r\n\r\n <div class=\"rounded-2xl border border-surface bg-surface/30 p-4\">\r\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\r\n <div class=\"space-y-1\">\r\n <div class=\"text-sm font-semibold\">{{ t(\"folderTitle\") }}</div>\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"folderDescription\") }}\r\n <span class=\"font-medium text-color\">{{ folderLabel() }}</span\r\n >.\r\n </div>\r\n @if (selectedFolderName()) {\r\n <div class=\"text-sm text-color\">\r\n {{ t(\"selectedFolder\") }}:\r\n <span class=\"font-medium\">{{ selectedFolderName() }}</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"flex flex-wrap items-center gap-2\">\r\n <mt-button\r\n [label]=\"t('selectFolder')\"\r\n icon=\"file.folder-plus\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"openFolderPicker(folderInput)\"\r\n />\r\n\r\n @if (folderItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('selectAll')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"setAllFolderItems(true)\"\r\n />\r\n <mt-button\r\n [label]=\"t('clearAll')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"setAllFolderItems(false)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (folderItems().length > 0) {\r\n <div\r\n class=\"grid max-h-[28rem] content-start grid-cols-1 gap-3 overflow-y-auto rounded-2xl border border-surface bg-content p-3 md:grid-cols-2 xl:grid-cols-4\"\r\n >\r\n <div\r\n class=\"col-span-full flex flex-wrap items-center justify-between gap-2 px-1\"\r\n >\r\n <div class=\"text-sm text-muted-color\">\r\n {{\r\n t(\"selectionSummary\", {\r\n selected: selectedCount(),\r\n total: folderItems().length,\r\n })\r\n }}\r\n </div>\r\n <div\r\n class=\"flex flex-wrap items-center gap-3 text-xs text-muted-color\"\r\n >\r\n <span>{{ t(\"uploadedCount\", { count: uploadedCount() }) }}</span>\r\n <span>{{ t(\"failedCount\", { count: failedCount() }) }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-span-full px-1\">\r\n <mt-text-field\r\n [ngModel]=\"targetRootFolderName()\"\r\n (ngModelChange)=\"targetRootFolderName.set($event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n [label]=\"t('targetFolderName')\"\r\n [placeholder]=\"t('targetFolderPlaceholder')\"\r\n [disabled]=\"submitting()\"\r\n />\r\n </div>\r\n\r\n @for (item of folderItems(); track item.id) {\r\n <mt-toggle-field\r\n toggleShape=\"card\"\r\n size=\"small\"\r\n [icon]=\"getFolderItemIcon(item)\"\r\n [label]=\"item.file.name\"\r\n [descriptionCard]=\"item.pathLabel\"\r\n [readonly]=\"item.uploading\"\r\n [ngModel]=\"item.enabled\"\r\n (ngModelChange)=\"toggleFolderItem(item.id, $event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n >\r\n <ng-template #toggleCardBottom>\r\n <div\r\n class=\"mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-surface pt-3 text-xs\"\r\n >\r\n <div class=\"text-muted-color\">\r\n {{ formatFileSize(item.file.size) }}\r\n </div>\r\n <div [class]=\"getFolderItemStatusClass(item)\">\r\n {{ getFolderItemStatus(item) }}\r\n </div>\r\n </div>\r\n\r\n @if (item.uploading) {\r\n <div class=\"mt-2 h-2 overflow-hidden rounded-full bg-surface\">\r\n <div\r\n class=\"h-full rounded-full bg-primary transition-all duration-200\"\r\n [style.width.%]=\"item.progress\"\r\n ></div>\r\n </div>\r\n }\r\n </ng-template>\r\n </mt-toggle-field>\r\n }\r\n </div>\r\n } @else {\r\n <div\r\n class=\"flex min-h-[16rem] items-center justify-center rounded-2xl border border-dashed border-surface bg-surface/20 p-6 text-center text-sm text-muted-color\"\r\n >\r\n {{ t(\"emptyFolderSelection\") }}\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <div\r\n [class]=\"[\r\n modal.footerClass,\r\n 'flex',\r\n 'items-center',\r\n 'justify-end',\r\n 'gap-2',\r\n ]\"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"cancel()\"\r\n />\r\n\r\n <mt-button\r\n [label]=\"primaryActionLabel()\"\r\n icon=\"general.upload-01\"\r\n [loading]=\"submitting()\"\r\n [disabled]=\"\r\n mode() === 'file'\r\n ? !uploadedFile()?.id && !uploadedFile()?.name\r\n : !selectedCount() ||\r\n !hasTargetRootFolderName() ||\r\n (pendingCount() === 0 && !canCloseWithUploads())\r\n \"\r\n (onClick)=\"submit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }, { kind: "component", type: UploadField, selector: "mt-upload-field", inputs: ["label", "title", "description", "endPoint", "size", "userImgClass", "shape", "accept", "isDragging", "fileSizeLimit", "readonly", "context"], outputs: ["isDraggingChange", "onChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] });
603
+ }
604
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibraryUploadDialog, decorators: [{
605
+ type: Component,
606
+ args: [{ selector: 'mt-document-library-upload-dialog', standalone: true, imports: [
607
+ CommonModule,
608
+ FormsModule,
609
+ Button,
610
+ TextField,
611
+ ToggleField,
612
+ UploadField,
613
+ TranslocoDirective,
614
+ ], template: "<ng-container *transloco=\"let t; prefix: 'document-library.modals.upload'\">\r\n <div [class]=\"[modal.contentClass, 'flex', 'flex-col', 'gap-4', 'p-4']\">\r\n @if (mode() === \"file\") {\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"fileIntro\") }}\r\n <span class=\"font-medium text-color\">{{ folderLabel() }}</span\r\n >.\r\n </div>\r\n\r\n <mt-upload-field\r\n [ngModel]=\"uploadedFile()\"\r\n (ngModelChange)=\"uploadedFile.set($event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n [shape]=\"'card'\"\r\n [title]=\"t('uploadFileTitle')\"\r\n [description]=\"t('uploadFileDescription')\"\r\n [endPoint]=\"uploadEndPoint()\"\r\n [context]=\"requestContext()\"\r\n />\r\n } @else {\r\n <input\r\n #folderInput\r\n type=\"file\"\r\n directory\r\n webkitdirectory\r\n multiple\r\n class=\"hidden\"\r\n (change)=\"onFolderSelect($event)\"\r\n />\r\n\r\n <div class=\"rounded-2xl border border-surface bg-surface/30 p-4\">\r\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\r\n <div class=\"space-y-1\">\r\n <div class=\"text-sm font-semibold\">{{ t(\"folderTitle\") }}</div>\r\n <div class=\"text-sm text-muted-color\">\r\n {{ t(\"folderDescription\") }}\r\n <span class=\"font-medium text-color\">{{ folderLabel() }}</span\r\n >.\r\n </div>\r\n @if (selectedFolderName()) {\r\n <div class=\"text-sm text-color\">\r\n {{ t(\"selectedFolder\") }}:\r\n <span class=\"font-medium\">{{ selectedFolderName() }}</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"flex flex-wrap items-center gap-2\">\r\n <mt-button\r\n [label]=\"t('selectFolder')\"\r\n icon=\"file.folder-plus\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"openFolderPicker(folderInput)\"\r\n />\r\n\r\n @if (folderItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('selectAll')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"setAllFolderItems(true)\"\r\n />\r\n <mt-button\r\n [label]=\"t('clearAll')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"setAllFolderItems(false)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (folderItems().length > 0) {\r\n <div\r\n class=\"grid max-h-[28rem] content-start grid-cols-1 gap-3 overflow-y-auto rounded-2xl border border-surface bg-content p-3 md:grid-cols-2 xl:grid-cols-4\"\r\n >\r\n <div\r\n class=\"col-span-full flex flex-wrap items-center justify-between gap-2 px-1\"\r\n >\r\n <div class=\"text-sm text-muted-color\">\r\n {{\r\n t(\"selectionSummary\", {\r\n selected: selectedCount(),\r\n total: folderItems().length,\r\n })\r\n }}\r\n </div>\r\n <div\r\n class=\"flex flex-wrap items-center gap-3 text-xs text-muted-color\"\r\n >\r\n <span>{{ t(\"uploadedCount\", { count: uploadedCount() }) }}</span>\r\n <span>{{ t(\"failedCount\", { count: failedCount() }) }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"col-span-full px-1\">\r\n <mt-text-field\r\n [ngModel]=\"targetRootFolderName()\"\r\n (ngModelChange)=\"targetRootFolderName.set($event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n [label]=\"t('targetFolderName')\"\r\n [placeholder]=\"t('targetFolderPlaceholder')\"\r\n [disabled]=\"submitting()\"\r\n />\r\n </div>\r\n\r\n @for (item of folderItems(); track item.id) {\r\n <mt-toggle-field\r\n toggleShape=\"card\"\r\n size=\"small\"\r\n [icon]=\"getFolderItemIcon(item)\"\r\n [label]=\"item.file.name\"\r\n [descriptionCard]=\"item.pathLabel\"\r\n [readonly]=\"item.uploading\"\r\n [ngModel]=\"item.enabled\"\r\n (ngModelChange)=\"toggleFolderItem(item.id, $event)\"\r\n [ngModelOptions]=\"{ standalone: true }\"\r\n >\r\n <ng-template #toggleCardBottom>\r\n <div\r\n class=\"mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-surface pt-3 text-xs\"\r\n >\r\n <div class=\"text-muted-color\">\r\n {{ formatFileSize(item.file.size) }}\r\n </div>\r\n <div [class]=\"getFolderItemStatusClass(item)\">\r\n {{ getFolderItemStatus(item) }}\r\n </div>\r\n </div>\r\n\r\n @if (item.uploading) {\r\n <div class=\"mt-2 h-2 overflow-hidden rounded-full bg-surface\">\r\n <div\r\n class=\"h-full rounded-full bg-primary transition-all duration-200\"\r\n [style.width.%]=\"item.progress\"\r\n ></div>\r\n </div>\r\n }\r\n </ng-template>\r\n </mt-toggle-field>\r\n }\r\n </div>\r\n } @else {\r\n <div\r\n class=\"flex min-h-[16rem] items-center justify-center rounded-2xl border border-dashed border-surface bg-surface/20 p-6 text-center text-sm text-muted-color\"\r\n >\r\n {{ t(\"emptyFolderSelection\") }}\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <div\r\n [class]=\"[\r\n modal.footerClass,\r\n 'flex',\r\n 'items-center',\r\n 'justify-end',\r\n 'gap-2',\r\n ]\"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"cancel()\"\r\n />\r\n\r\n <mt-button\r\n [label]=\"primaryActionLabel()\"\r\n icon=\"general.upload-01\"\r\n [loading]=\"submitting()\"\r\n [disabled]=\"\r\n mode() === 'file'\r\n ? !uploadedFile()?.id && !uploadedFile()?.name\r\n : !selectedCount() ||\r\n !hasTargetRootFolderName() ||\r\n (pendingCount() === 0 && !canCloseWithUploads())\r\n \"\r\n (onClick)=\"submit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
615
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], folderLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "folderLabel", required: false }] }], uploadEndPoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadEndPoint", required: false }] }], requestContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestContext", required: false }] }] } });
616
+
617
+ class DocumentLibrary {
618
+ api = inject(DocumentLibraryApiService);
619
+ confirmationService = inject(ConfirmationService);
620
+ modalService = inject(ModalService);
621
+ toast = inject(ToastService);
622
+ transloco = inject(TranslocoService);
623
+ levelId = input.required(...(ngDevMode ? [{ debugName: "levelId" }] : []));
624
+ levelDataId = input(null, ...(ngDevMode ? [{ debugName: "levelDataId" }] : []));
625
+ title = input('', ...(ngDevMode ? [{ debugName: "title" }] : []));
626
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : []));
627
+ lang = input('en', ...(ngDevMode ? [{ debugName: "lang" }] : []));
628
+ requestContext = input(undefined, ...(ngDevMode ? [{ debugName: "requestContext" }] : []));
629
+ uploadEndPoint = input('uploader', ...(ngDevMode ? [{ debugName: "uploadEndPoint" }] : []));
630
+ emptyTitle = input('', ...(ngDevMode ? [{ debugName: "emptyTitle" }] : []));
631
+ emptyDescription = input('', ...(ngDevMode ? [{ debugName: "emptyDescription" }] : []));
632
+ loaded = output();
633
+ errored = output();
634
+ selectionChanged = output();
635
+ actionExecuted = output();
636
+ nameCellTpl = viewChild('nameCellTpl', ...(ngDevMode ? [{ debugName: "nameCellTpl" }] : []));
637
+ updatedCellTpl = viewChild('updatedCellTpl', ...(ngDevMode ? [{ debugName: "updatedCellTpl" }] : []));
638
+ actionsCellTpl = viewChild('actionsCellTpl', ...(ngDevMode ? [{ debugName: "actionsCellTpl" }] : []));
639
+ addPopover = viewChild('addPopover', ...(ngDevMode ? [{ debugName: "addPopover" }] : []));
640
+ folderPopover = viewChild('folderPopover', ...(ngDevMode ? [{ debugName: "folderPopover" }] : []));
641
+ contextPopover = viewChild('contextPopover', ...(ngDevMode ? [{ debugName: "contextPopover" }] : []));
642
+ roots = signal([], ...(ngDevMode ? [{ debugName: "roots" }] : []));
643
+ folderRegistry = signal(new Map(), ...(ngDevMode ? [{ debugName: "folderRegistry" }] : []));
644
+ folderEntriesCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "folderEntriesCache" }] : []));
645
+ expandedFolderKeys = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedFolderKeys" }] : []));
646
+ busyKeys = signal(new Set(), ...(ngDevMode ? [{ debugName: "busyKeys" }] : []));
647
+ selectedFolderId = signal(null, ...(ngDevMode ? [{ debugName: "selectedFolderId" }] : []));
648
+ selectedEntry = signal(null, ...(ngDevMode ? [{ debugName: "selectedEntry" }] : []));
649
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
650
+ contextMenuItems = signal([], ...(ngDevMode ? [{ debugName: "contextMenuItems" }] : []));
651
+ railSearch = signal('', ...(ngDevMode ? [{ debugName: "railSearch" }] : []));
652
+ activeLanguage = toSignal(this.transloco.langChanges$, {
653
+ initialValue: this.transloco.getActiveLang() || 'en',
654
+ });
655
+ canMutateLevelData = computed(() => this.levelDataId() != null, ...(ngDevMode ? [{ debugName: "canMutateLevelData" }] : []));
656
+ uiLang = computed(() => {
657
+ const activeLang = this.activeLanguage();
658
+ return activeLang === 'ar'
659
+ ? 'ar'
660
+ : activeLang === 'en'
661
+ ? 'en'
662
+ : this.lang();
663
+ }, ...(ngDevMode ? [{ debugName: "uiLang" }] : []));
664
+ resolvedTitle = computed(() => this.title().trim() || this.translate('title'), ...(ngDevMode ? [{ debugName: "resolvedTitle" }] : []));
665
+ resolvedEmptyTitle = computed(() => this.emptyTitle().trim() || this.translate('empty.title'), ...(ngDevMode ? [{ debugName: "resolvedEmptyTitle" }] : []));
666
+ resolvedEmptyDescription = computed(() => this.emptyDescription().trim() || this.translate('empty.description'), ...(ngDevMode ? [{ debugName: "resolvedEmptyDescription" }] : []));
667
+ capabilities = computed(() => ({
668
+ canCreateRootFolders: true,
669
+ canUploadFiles: this.canMutateLevelData(),
670
+ canDownloadFiles: true,
671
+ canDeleteFiles: this.canMutateLevelData(),
672
+ }), ...(ngDevMode ? [{ debugName: "capabilities" }] : []));
673
+ columns = computed(() => {
674
+ const nameCellTpl = this.nameCellTpl();
675
+ const updatedCellTpl = this.updatedCellTpl();
676
+ const actionsCellTpl = this.actionsCellTpl();
677
+ if (!nameCellTpl || !updatedCellTpl || !actionsCellTpl) {
678
+ return [];
679
+ }
680
+ return [
681
+ {
682
+ key: 'name',
683
+ label: this.translate('columns.name'),
684
+ type: 'custom',
685
+ customCellTpl: nameCellTpl,
686
+ },
687
+ {
688
+ key: 'owner',
689
+ label: this.translate('columns.owner'),
690
+ },
691
+ {
692
+ key: 'size',
693
+ label: this.translate('columns.size'),
694
+ },
695
+ {
696
+ key: 'updatedAt',
697
+ label: this.translate('columns.lastUpdated'),
698
+ type: 'custom',
699
+ customCellTpl: updatedCellTpl,
700
+ },
701
+ {
702
+ key: 'actions',
703
+ label: this.translate('columns.actions'),
704
+ type: 'custom',
705
+ customCellTpl: actionsCellTpl,
706
+ },
707
+ ];
708
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : []));
709
+ selectedFolder = computed(() => {
710
+ const folderId = this.selectedFolderId();
711
+ if (folderId == null) {
712
+ return null;
713
+ }
714
+ return this.folderRegistry().get(this.toFolderKey(folderId)) ?? null;
715
+ }, ...(ngDevMode ? [{ debugName: "selectedFolder" }] : []));
716
+ currentEntries = computed(() => {
717
+ const folderId = this.selectedFolderId();
718
+ if (folderId == null) {
719
+ return this.roots().map((folder) => toDocumentLibraryFolderEntry(folder));
720
+ }
721
+ return this.folderEntriesCache().get(this.toFolderKey(folderId)) ?? [];
722
+ }, ...(ngDevMode ? [{ debugName: "currentEntries" }] : []));
723
+ tableRows = computed(() => this.currentEntries().map((entry) => ({
724
+ id: this.getEntryKey(entry),
725
+ entry,
726
+ name: this.getEntryTitle(entry),
727
+ owner: this.getEntryOwner(entry),
728
+ size: entry.isFolder ? '-' : this.formatFileSize(entry.size),
729
+ updatedAt: this.getEntryUpdatedAt(entry),
730
+ })), ...(ngDevMode ? [{ debugName: "tableRows" }] : []));
731
+ breadcrumbs = computed(() => {
732
+ const items = [
733
+ {
734
+ icon: 'general.home-05',
735
+ label: this.resolvedTitle(),
736
+ folderId: null,
737
+ },
738
+ ];
739
+ this.getFolderTrail(this.selectedFolderId()).forEach((folder) => {
740
+ items.push({
741
+ label: this.getFolderLabel(folder),
742
+ folderId: folder.id,
743
+ });
744
+ });
745
+ return items;
746
+ }, ...(ngDevMode ? [{ debugName: "breadcrumbs" }] : []));
747
+ currentFolderTitle = computed(() => {
748
+ const selectedFolder = this.selectedFolder();
749
+ return selectedFolder
750
+ ? this.getFolderLabel(selectedFolder)
751
+ : this.resolvedTitle();
752
+ }, ...(ngDevMode ? [{ debugName: "currentFolderTitle" }] : []));
753
+ currentFolderSubtitle = computed(() => {
754
+ const selectedFolder = this.selectedFolder();
755
+ if (!selectedFolder) {
756
+ return this.translate('summary.rootFolders', {
757
+ count: this.roots().length,
758
+ });
759
+ }
760
+ const entries = this.currentEntries();
761
+ const folderCount = entries.filter((entry) => entry.isFolder).length;
762
+ const fileCount = entries.length - folderCount;
763
+ return this.translate('summary.folderContents', {
764
+ folderCount,
765
+ fileCount,
766
+ });
767
+ }, ...(ngDevMode ? [{ debugName: "currentFolderSubtitle" }] : []));
768
+ rootSummary = computed(() => {
769
+ return this.translate('summary.rootFolders', {
770
+ count: this.roots().length,
771
+ });
772
+ }, ...(ngDevMode ? [{ debugName: "rootSummary" }] : []));
773
+ visibleRoots = computed(() => {
774
+ const query = this.railSearch().trim().toLowerCase();
775
+ if (!query) {
776
+ return this.roots();
777
+ }
778
+ return this.roots().filter((folder) => this.getFolderLabel(folder).toLowerCase().includes(query));
779
+ }, ...(ngDevMode ? [{ debugName: "visibleRoots" }] : []));
780
+ addMenuItems = computed(() => [
781
+ {
782
+ label: this.translate('actions.newFolder'),
783
+ icon: 'file.folder-plus',
784
+ command: () => this.openCreateFolderDialog(this.selectedFolder()),
785
+ visible: this.canCreateFolderInSelection(),
786
+ },
787
+ {
788
+ label: this.translate('actions.fileUpload'),
789
+ icon: 'general.upload-01',
790
+ command: () => this.openUploadDialog('file'),
791
+ visible: this.canUploadIntoCurrentFolder(),
792
+ },
793
+ {
794
+ label: this.translate('actions.folderUpload'),
795
+ icon: 'file.folder',
796
+ command: () => this.openUploadDialog('folder'),
797
+ visible: this.canUploadIntoCurrentFolder(),
798
+ },
799
+ ], ...(ngDevMode ? [{ debugName: "addMenuItems" }] : []));
800
+ currentFolderMenuItems = computed(() => {
801
+ const folder = this.selectedFolder();
802
+ return folder ? this.getFolderMenuItems(folder) : [];
803
+ }, ...(ngDevMode ? [{ debugName: "currentFolderMenuItems" }] : []));
804
+ constructor() {
805
+ effect(() => {
806
+ this.levelId();
807
+ this.levelDataId();
808
+ this.requestContext();
809
+ untracked(() => {
810
+ void this.reload();
811
+ });
812
+ });
813
+ }
814
+ async reload() {
815
+ try {
816
+ await this.withBusy('roots', async () => {
817
+ this.error.set(null);
818
+ this.roots.set([]);
819
+ this.folderRegistry.set(new Map());
820
+ this.folderEntriesCache.set(new Map());
821
+ this.expandedFolderKeys.set(new Set());
822
+ this.selectedFolderId.set(null);
823
+ this.selectedEntry.set(null);
824
+ const folders = (await this.resolveAsync(this.api.loadRoots(this.levelId(), this.levelDataId(), this.requestContext()))).map((folder) => normalizeDocumentLibraryFolder(folder));
825
+ this.roots.set(folders);
826
+ this.upsertFolders(folders);
827
+ });
828
+ this.loaded.emit();
829
+ this.actionExecuted.emit({ action: 'reload', target: null });
830
+ }
831
+ catch (error) {
832
+ this.handleError(error, this.translate('errors.loadLibrary'));
833
+ }
834
+ }
835
+ async goToRoot() {
836
+ this.selectedFolderId.set(null);
837
+ this.selectedEntry.set(null);
838
+ this.selectionChanged.emit({
839
+ folder: null,
840
+ entry: null,
841
+ });
842
+ }
843
+ async openFolder(folder) {
844
+ this.selectedFolderId.set(folder.id);
845
+ this.selectedEntry.set(toDocumentLibraryFolderEntry(folder));
846
+ this.expandFolderPath(folder);
847
+ if (!this.folderEntriesCache().has(this.toFolderKey(folder.id))) {
848
+ await this.loadFolderEntries(folder.id);
849
+ }
850
+ this.selectionChanged.emit({
851
+ folder,
852
+ entry: toDocumentLibraryFolderEntry(folder),
853
+ });
854
+ this.actionExecuted.emit({
855
+ action: 'open-folder',
856
+ target: folder,
857
+ });
858
+ }
859
+ async toggleExpanded(folder) {
860
+ const folderKey = this.toFolderKey(folder.id);
861
+ if (!this.expandedFolderKeys().has(folderKey)) {
862
+ if (!this.folderEntriesCache().has(folderKey)) {
863
+ await this.loadFolderEntries(folder.id);
864
+ }
865
+ this.expandedFolderKeys.update((state) => {
866
+ const next = new Set(state);
867
+ next.add(folderKey);
868
+ return next;
869
+ });
870
+ return;
871
+ }
872
+ this.expandedFolderKeys.update((state) => {
873
+ const next = new Set(state);
874
+ next.delete(folderKey);
875
+ return next;
876
+ });
877
+ }
878
+ async onRailFolderClick(folder) {
879
+ await this.openFolder(folder);
880
+ }
881
+ async onRowClick(row) {
882
+ this.selectedEntry.set(row.entry);
883
+ if (row.entry.isFolder) {
884
+ await this.openFolder(row.entry);
885
+ return;
886
+ }
887
+ this.selectionChanged.emit({
888
+ folder: this.selectedFolder(),
889
+ entry: row.entry,
890
+ });
891
+ }
892
+ openAddPopover(event) {
893
+ if (!this.hasVisibleActionItems(this.addMenuItems())) {
894
+ return;
895
+ }
896
+ this.addPopover()?.toggle(event);
897
+ }
898
+ openFolderPopover(event) {
899
+ if (!this.hasVisibleActionItems(this.currentFolderMenuItems())) {
900
+ return;
901
+ }
902
+ this.folderPopover()?.toggle(event);
903
+ }
904
+ openContextMenu(event, target) {
905
+ event.preventDefault();
906
+ event.stopPropagation();
907
+ const items = this.getTargetMenuItems(target);
908
+ if (!this.hasVisibleActionItems(items)) {
909
+ return;
910
+ }
911
+ this.contextMenuItems.set(items);
912
+ this.showPopover(this.contextPopover(), event);
913
+ }
914
+ executePopoverItem(item, popover) {
915
+ if (item.disabled) {
916
+ return;
917
+ }
918
+ popover?.hide();
919
+ item.command?.();
920
+ }
921
+ openCreateFolderDialog(parent) {
922
+ if (!this.canCreateFolderInSelection(parent)) {
923
+ return;
924
+ }
925
+ this.openFolderDialog('create', {
926
+ contextLabel: parent ? this.getFolderLabel(parent) : this.resolvedTitle(),
927
+ initialName: '',
928
+ onSubmit: (folderName) => this.createFolder(parent, folderName),
929
+ });
930
+ }
931
+ openRenameFolderDialog(folder) {
932
+ if (!this.canRenameFolder(folder)) {
933
+ return;
934
+ }
935
+ this.openFolderDialog('rename', {
936
+ contextLabel: this.getFolderLabel(folder),
937
+ initialName: this.getFolderLabel(folder),
938
+ onSubmit: (folderName) => this.renameFolder(folder, folderName),
939
+ });
940
+ }
941
+ async createFolder(parent, folderName) {
942
+ try {
943
+ const created = await this.createFolderInBackend(parent, folderName);
944
+ this.toast.success(this.translate('messages.folderCreated', {
945
+ name: this.getFolderLabel(created),
946
+ }));
947
+ this.actionExecuted.emit({
948
+ action: 'create-folder',
949
+ target: created,
950
+ });
951
+ }
952
+ catch (error) {
953
+ this.handleError(error, this.translate('errors.createFolder'));
954
+ }
955
+ }
956
+ async renameFolder(target, folderName) {
957
+ const levelDataId = this.levelDataId();
958
+ if (!folderName || levelDataId == null) {
959
+ return;
960
+ }
961
+ try {
962
+ const response = await this.withBusy(this.renameFolderBusyKey(target.id), async () => this.resolveAsync(this.api.renameFolder(this.levelId(), levelDataId, {
963
+ folderId: target.id,
964
+ folderName,
965
+ }, this.requestContext())));
966
+ const updated = normalizeDocumentLibraryFolder(response && typeof response === 'object' && 'id' in response
967
+ ? response
968
+ : {
969
+ ...target,
970
+ label: normalizeDocumentLibraryLabel({
971
+ ...target.label,
972
+ display: folderName,
973
+ en: target.label.en ?? folderName,
974
+ ar: target.label.ar ?? folderName,
975
+ }),
976
+ });
977
+ this.replaceFolder(updated);
978
+ this.syncSelectedFolderEntry(updated);
979
+ this.toast.success(this.translate('messages.folderRenamed', {
980
+ name: this.getFolderLabel(updated),
981
+ }));
982
+ this.actionExecuted.emit({
983
+ action: 'rename-folder',
984
+ target: updated,
985
+ });
986
+ }
987
+ catch (error) {
988
+ this.handleError(error, this.translate('errors.renameFolder'));
989
+ }
990
+ }
991
+ confirmDeleteFolder(folder) {
992
+ this.confirmationService.confirmDelete({
993
+ type: 'dialog',
994
+ accept: () => {
995
+ void this.deleteFolder(folder);
996
+ },
997
+ });
998
+ }
999
+ async deleteFolder(folder) {
1000
+ const levelDataId = this.levelDataId();
1001
+ if (levelDataId == null) {
1002
+ return;
1003
+ }
1004
+ try {
1005
+ await this.withBusy(this.deleteFolderBusyKey(folder.id), async () => {
1006
+ await this.resolveAsync(this.api.deleteFolder(this.levelId(), levelDataId, {
1007
+ folderId: folder.id,
1008
+ }, this.requestContext()));
1009
+ });
1010
+ const removedFolderKeys = this.removeFolderTree(folder.id);
1011
+ if (this.isSelectedFolderInTree(removedFolderKeys)) {
1012
+ await this.goToRoot();
1013
+ }
1014
+ else {
1015
+ this.clearSelectionForRemovedFolders(removedFolderKeys);
1016
+ }
1017
+ this.toast.success(this.translate('messages.folderDeleted', {
1018
+ name: this.getFolderLabel(folder),
1019
+ }));
1020
+ this.actionExecuted.emit({
1021
+ action: 'delete-folder',
1022
+ target: folder,
1023
+ });
1024
+ }
1025
+ catch (error) {
1026
+ this.handleError(error, this.translate('errors.deleteFolder'));
1027
+ }
1028
+ }
1029
+ confirmDeleteFile(file) {
1030
+ this.confirmationService.confirmDelete({
1031
+ type: 'dialog',
1032
+ accept: () => {
1033
+ void this.deleteFile(file);
1034
+ },
1035
+ });
1036
+ }
1037
+ async deleteFile(file) {
1038
+ const levelDataId = this.levelDataId();
1039
+ if (levelDataId == null) {
1040
+ return;
1041
+ }
1042
+ try {
1043
+ await this.withBusy(this.deleteFileBusyKey(file.id), async () => {
1044
+ await this.resolveAsync(this.api.deleteDocument(this.levelId(), levelDataId, {
1045
+ fileId: file.id,
1046
+ }, this.requestContext()));
1047
+ });
1048
+ const folderId = this.selectedFolderId();
1049
+ if (folderId != null) {
1050
+ this.patchFolderEntries(folderId, (entries) => entries.filter((entry) => !(entry.isFolder === false && entry.id === file.id)));
1051
+ }
1052
+ const selectedEntry = this.selectedEntry();
1053
+ if (selectedEntry &&
1054
+ !selectedEntry.isFolder &&
1055
+ selectedEntry.id === file.id) {
1056
+ this.selectedEntry.set(null);
1057
+ }
1058
+ this.toast.success(this.translate('messages.fileDeleted', {
1059
+ name: file.fileName,
1060
+ }));
1061
+ this.actionExecuted.emit({
1062
+ action: 'delete-file',
1063
+ target: file,
1064
+ });
1065
+ }
1066
+ catch (error) {
1067
+ this.handleError(error, this.translate('errors.deleteFile'));
1068
+ }
1069
+ }
1070
+ async downloadFile(file) {
1071
+ try {
1072
+ await this.resolveAsync(this.api.downloadFile({
1073
+ file,
1074
+ }, this.requestContext()));
1075
+ this.actionExecuted.emit({
1076
+ action: 'download-file',
1077
+ target: file,
1078
+ });
1079
+ }
1080
+ catch (error) {
1081
+ this.handleError(error, this.translate('errors.downloadFile', { name: file.fileName }));
1082
+ }
1083
+ }
1084
+ openUploadDialog(mode = 'file') {
1085
+ const selectedFolder = this.selectedFolder();
1086
+ if (!selectedFolder || !this.canUploadIntoFolder(selectedFolder)) {
1087
+ return;
1088
+ }
1089
+ const ref = this.modalService.openModal(DocumentLibraryUploadDialog, 'dialog', {
1090
+ header: mode === 'folder'
1091
+ ? this.translate('modals.upload.headerFolder')
1092
+ : this.translate('modals.upload.headerFile'),
1093
+ styleClass: mode === 'folder' ? '!w-[60rem]' : '!w-[34rem]',
1094
+ inputValues: {
1095
+ mode,
1096
+ folderLabel: this.getFolderLabel(selectedFolder),
1097
+ uploadEndPoint: this.uploadEndPoint(),
1098
+ requestContext: this.requestContext(),
1099
+ },
1100
+ });
1101
+ if (!ref) {
1102
+ return;
1103
+ }
1104
+ ref.onClose.subscribe((result) => {
1105
+ if (result?.uploads?.length) {
1106
+ void this.attachUploadedDocuments(selectedFolder, result.uploads);
1107
+ }
1108
+ });
1109
+ }
1110
+ foldersForRail(parentId) {
1111
+ const query = this.railSearch().trim().toLowerCase();
1112
+ const folders = parentId == null
1113
+ ? this.visibleRoots()
1114
+ : (this.folderEntriesCache().get(this.toFolderKey(parentId)) ?? [])
1115
+ .filter((entry) => entry.isFolder)
1116
+ .map((entry) => normalizeDocumentLibraryFolder(entry));
1117
+ if (!query || parentId == null) {
1118
+ return folders;
1119
+ }
1120
+ return folders.filter((folder) => this.getFolderLabel(folder).toLowerCase().includes(query));
1121
+ }
1122
+ isExpanded(folderId) {
1123
+ return this.expandedFolderKeys().has(this.toFolderKey(folderId));
1124
+ }
1125
+ isSelectedFolder(folderId) {
1126
+ const selectedFolderId = this.selectedFolderId();
1127
+ if (selectedFolderId == null) {
1128
+ return false;
1129
+ }
1130
+ return this.toFolderKey(selectedFolderId) === this.toFolderKey(folderId);
1131
+ }
1132
+ hasLoadedChildren(folderId) {
1133
+ return this.folderEntriesCache().has(this.toFolderKey(folderId));
1134
+ }
1135
+ folderHasChildFolders(folderId) {
1136
+ const entries = this.folderEntriesCache().get(this.toFolderKey(folderId));
1137
+ if (!entries) {
1138
+ return true;
1139
+ }
1140
+ return entries.some((entry) => entry.isFolder);
1141
+ }
1142
+ canCreateFolderInSelection(folder = this.selectedFolder()) {
1143
+ if (folder == null) {
1144
+ return !!this.capabilities().canCreateRootFolders;
1145
+ }
1146
+ return !!folder.isEditableFolder;
1147
+ }
1148
+ canUploadIntoCurrentFolder() {
1149
+ return this.canUploadIntoFolder(this.selectedFolder());
1150
+ }
1151
+ canUploadIntoFolder(folder) {
1152
+ if (!folder) {
1153
+ return false;
1154
+ }
1155
+ return (!!this.capabilities().canUploadFiles &&
1156
+ !!folder.isEditableFolder &&
1157
+ !folder.isPrimaryFolder);
1158
+ }
1159
+ canDeleteFolder(folder) {
1160
+ return (this.levelDataId() != null &&
1161
+ !!folder.isEditableFolder &&
1162
+ !folder.isPrimaryFolder);
1163
+ }
1164
+ canDeleteFile(_file) {
1165
+ return !!this.capabilities().canDeleteFiles;
1166
+ }
1167
+ canDownloadFile(_file) {
1168
+ return !!this.capabilities().canDownloadFiles;
1169
+ }
1170
+ getFolderLabel(folder) {
1171
+ return (getDocumentLibraryFolderLabel(folder, this.uiLang()).trim() ||
1172
+ this.translate('fallback.folder', { id: folder.id }));
1173
+ }
1174
+ getEntryTitle(entry) {
1175
+ return entry.isFolder
1176
+ ? this.getFolderLabel(entry)
1177
+ : getDocumentLibraryEntryTitle(entry, this.uiLang());
1178
+ }
1179
+ getEntryOwner(entry) {
1180
+ if (entry.isFolder) {
1181
+ return entry.isEditableFolder
1182
+ ? this.translate('owner.workspace')
1183
+ : this.translate('owner.system');
1184
+ }
1185
+ return entry.createdBy ?? this.translate('owner.unknown');
1186
+ }
1187
+ getEntryUpdatedAt(entry) {
1188
+ return entry.isFolder ? '' : entry.created;
1189
+ }
1190
+ getEntryKey(entry) {
1191
+ return entry.isFolder
1192
+ ? `folder:${this.toFolderKey(entry.id)}`
1193
+ : `file:${entry.id}`;
1194
+ }
1195
+ getFolderMenuItems(folder) {
1196
+ return [
1197
+ {
1198
+ label: this.translate('actions.open'),
1199
+ icon: 'file.folder',
1200
+ command: () => void this.openFolder(folder),
1201
+ },
1202
+ {
1203
+ label: this.translate('actions.newSubfolder'),
1204
+ icon: 'file.folder-plus',
1205
+ command: () => this.openCreateFolderDialog(folder),
1206
+ visible: this.canCreateFolderInSelection(folder),
1207
+ },
1208
+ {
1209
+ label: this.translate('actions.uploadFile'),
1210
+ icon: 'general.upload-01',
1211
+ command: () => {
1212
+ void this.openFolder(folder).then(() => this.openUploadDialog('file'));
1213
+ },
1214
+ visible: this.canUploadIntoFolder(folder),
1215
+ },
1216
+ {
1217
+ label: this.translate('actions.uploadFolder'),
1218
+ icon: 'file.folder',
1219
+ command: () => {
1220
+ void this.openFolder(folder).then(() => this.openUploadDialog('folder'));
1221
+ },
1222
+ visible: this.canUploadIntoFolder(folder),
1223
+ },
1224
+ {
1225
+ label: this.translate('actions.rename'),
1226
+ icon: 'general.edit-02',
1227
+ command: () => this.openRenameFolderDialog(folder),
1228
+ visible: this.canRenameFolder(folder),
1229
+ },
1230
+ {
1231
+ label: this.translate('actions.delete'),
1232
+ icon: 'general.trash-01',
1233
+ severity: 'danger',
1234
+ command: () => this.confirmDeleteFolder(folder),
1235
+ visible: this.canDeleteFolder(folder),
1236
+ },
1237
+ ];
1238
+ }
1239
+ getEntryMenuItems(entry) {
1240
+ if (entry.isFolder) {
1241
+ return this.getFolderMenuItems(entry);
1242
+ }
1243
+ return [
1244
+ {
1245
+ label: this.translate('actions.download'),
1246
+ icon: 'general.download-01',
1247
+ command: () => void this.downloadFile(entry),
1248
+ visible: this.canDownloadFile(entry),
1249
+ },
1250
+ {
1251
+ label: this.translate('actions.delete'),
1252
+ icon: 'general.trash-01',
1253
+ severity: 'danger',
1254
+ command: () => this.confirmDeleteFile(entry),
1255
+ visible: this.canDeleteFile(entry),
1256
+ },
1257
+ ];
1258
+ }
1259
+ getFolderIcon(folder) {
1260
+ if (!folder.isEditableFolder && folder.isPrimaryFolder) {
1261
+ return 'file.folder-lock';
1262
+ }
1263
+ if (!folder.isEditableFolder) {
1264
+ return 'security.folder-shield';
1265
+ }
1266
+ return this.isSelectedFolder(folder.id)
1267
+ ? 'file.folder'
1268
+ : 'file.folder-closed';
1269
+ }
1270
+ getFileIcon(entry) {
1271
+ const extension = this.getFileExtension(entry);
1272
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(extension)) {
1273
+ return 'image.image-03';
1274
+ }
1275
+ if (['ts', 'js', 'json', 'html', 'css', 'scss'].includes(extension)) {
1276
+ return 'dev.file-code-02';
1277
+ }
1278
+ if (entry.contentType.toLowerCase().includes('pdf')) {
1279
+ return 'file.file-06';
1280
+ }
1281
+ return 'file.file-01';
1282
+ }
1283
+ getFileAvatarPalette(entry) {
1284
+ const extension = this.getFileExtension(entry);
1285
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(extension)) {
1286
+ return {
1287
+ background: '#d1fae5',
1288
+ color: '#047857',
1289
+ };
1290
+ }
1291
+ if (entry.contentType.toLowerCase().includes('pdf')) {
1292
+ return {
1293
+ background: '#ffe4e6',
1294
+ color: '#be123c',
1295
+ };
1296
+ }
1297
+ if (['xls', 'xlsx', 'csv'].includes(extension)) {
1298
+ return {
1299
+ background: '#dcfce7',
1300
+ color: '#15803d',
1301
+ };
1302
+ }
1303
+ if (['doc', 'docx'].includes(extension)) {
1304
+ return {
1305
+ background: '#dbeafe',
1306
+ color: '#1d4ed8',
1307
+ };
1308
+ }
1309
+ return {
1310
+ background: '#f1f5f9',
1311
+ color: '#334155',
1312
+ };
1313
+ }
1314
+ getHomeAvatarStyle() {
1315
+ return `
1316
+ --p-avatar-background: var(--p-primary-50);
1317
+ --p-avatar-color: var(--p-primary-700);
1318
+ `;
1319
+ }
1320
+ getFolderAvatarStyle(folder) {
1321
+ if (!folder.isEditableFolder && folder.isPrimaryFolder) {
1322
+ return `
1323
+ --p-avatar-background: color-mix(in srgb, var(--p-primary-color) 12%, white);
1324
+ --p-avatar-color: color-mix(in srgb, var(--p-primary-color) 78%, black);
1325
+ `;
1326
+ }
1327
+ if (!folder.isEditableFolder) {
1328
+ return `
1329
+ --p-avatar-background: color-mix(in srgb, var(--p-surface-500) 14%, white);
1330
+ --p-avatar-color: color-mix(in srgb, var(--p-surface-900) 72%, black);
1331
+ `;
1332
+ }
1333
+ return `
1334
+ --p-avatar-background: #fef3c7;
1335
+ --p-avatar-color: #d97706;
1336
+ `;
1337
+ }
1338
+ getFileAvatarStyle(entry) {
1339
+ const palette = this.getFileAvatarPalette(entry);
1340
+ return `
1341
+ --p-avatar-background: ${palette.background};
1342
+ --p-avatar-color: ${palette.color};
1343
+ `;
1344
+ }
1345
+ getActionAvatarStyle() {
1346
+ return `
1347
+ --p-avatar-background: color-mix(in srgb, var(--p-surface-100) 88%, white);
1348
+ --p-avatar-color: var(--p-surface-500);
1349
+ `;
1350
+ }
1351
+ getActionPopoverStyle() {
1352
+ return {
1353
+ width: '15rem',
1354
+ };
1355
+ }
1356
+ getActionItemButtonClass(item) {
1357
+ const baseClass = 'flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm text-left transition-colors';
1358
+ if (item.disabled) {
1359
+ return `${baseClass} cursor-not-allowed opacity-45`;
1360
+ }
1361
+ if (item.severity === 'danger') {
1362
+ return `${baseClass} text-red-600 hover:bg-red-50`;
1363
+ }
1364
+ return `${baseClass} text-color hover:bg-surface`;
1365
+ }
1366
+ getActionItemAvatarStyle(item) {
1367
+ if (item.disabled) {
1368
+ return `
1369
+ --p-avatar-background: color-mix(in srgb, var(--p-surface-200) 76%, white);
1370
+ --p-avatar-color: var(--p-surface-400);
1371
+ `;
1372
+ }
1373
+ if (item.severity === 'danger') {
1374
+ return `
1375
+ --p-avatar-background: color-mix(in srgb, #ef4444 12%, white);
1376
+ --p-avatar-color: #dc2626;
1377
+ `;
1378
+ }
1379
+ return `
1380
+ --p-avatar-background: color-mix(in srgb, var(--p-primary-color) 10%, white);
1381
+ --p-avatar-color: color-mix(in srgb, var(--p-primary-color) 82%, black);
1382
+ `;
1383
+ }
1384
+ getVisibleActionItems(items) {
1385
+ return items.filter((item) => item.visible !== false && !item.separator);
1386
+ }
1387
+ hasVisibleActionItems(items) {
1388
+ return this.getVisibleActionItems(items).length > 0;
1389
+ }
1390
+ trackActionItem(item, index) {
1391
+ return item.id ?? item.label ?? index;
1392
+ }
1393
+ getIconButtonClass() {
1394
+ return 'inline-flex items-center justify-center border-0 bg-transparent p-0 cursor-pointer disabled:cursor-not-allowed disabled:opacity-55';
1395
+ }
1396
+ getRailItemClass(active) {
1397
+ const baseClass = 'w-full rounded-xl text-left text-sm font-medium text-color transition-[background-color,color,box-shadow] duration-150 hover:bg-[color-mix(in_srgb,var(--p-primary-color)_8%,transparent)]';
1398
+ if (!active) {
1399
+ return baseClass;
1400
+ }
1401
+ return `${baseClass} bg-[color-mix(in_srgb,var(--p-primary-color)_14%,white)] shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--p-primary-color)_20%,transparent)] text-[color-mix(in_srgb,var(--p-primary-color)_82%,black)]`;
1402
+ }
1403
+ getFileExtension(entry) {
1404
+ return getDocumentLibraryFileExtension(entry.fileName);
1405
+ }
1406
+ formatFileSize(size) {
1407
+ return formatDocumentLibraryFileSize(size);
1408
+ }
1409
+ onBreadcrumbClick(item) {
1410
+ const targetItem = item;
1411
+ if (targetItem.folderId == null) {
1412
+ void this.goToRoot();
1413
+ return;
1414
+ }
1415
+ const targetFolder = this.folderRegistry().get(this.toFolderKey(targetItem.folderId)) ?? null;
1416
+ if (!targetFolder) {
1417
+ void this.goToRoot();
1418
+ return;
1419
+ }
1420
+ void this.openFolder(targetFolder);
1421
+ }
1422
+ isBusy(key) {
1423
+ return this.busyKeys().has(key);
1424
+ }
1425
+ createFolderBusyKey(folderId) {
1426
+ return `create-folder:${this.toFolderKey(folderId)}`;
1427
+ }
1428
+ renameFolderBusyKey(folderId) {
1429
+ return `rename-folder:${this.toFolderKey(folderId)}`;
1430
+ }
1431
+ loadFolderBusyKey(folderId) {
1432
+ return `load-folder:${this.toFolderKey(folderId)}`;
1433
+ }
1434
+ openFolderDialog(mode, options) {
1435
+ const ref = this.modalService.openModal(DocumentLibraryFolderDialog, 'dialog', {
1436
+ header: mode === 'create'
1437
+ ? this.translate('modals.folder.headerCreate')
1438
+ : this.translate('modals.folder.headerRename'),
1439
+ styleClass: '!w-[28rem]',
1440
+ inputValues: {
1441
+ mode,
1442
+ contextLabel: options.contextLabel,
1443
+ initialName: options.initialName,
1444
+ },
1445
+ });
1446
+ if (!ref) {
1447
+ return;
1448
+ }
1449
+ ref.onClose.subscribe((result) => {
1450
+ if (result?.folderName) {
1451
+ void options.onSubmit(result.folderName);
1452
+ }
1453
+ });
1454
+ }
1455
+ async loadFolderEntries(folderId) {
1456
+ try {
1457
+ await this.fetchFolderEntries(folderId);
1458
+ }
1459
+ catch (error) {
1460
+ this.handleError(error, this.translate('errors.loadFolderContents'));
1461
+ }
1462
+ }
1463
+ async fetchFolderEntries(folderId) {
1464
+ const folderKey = this.toFolderKey(folderId);
1465
+ const entries = await this.withBusy(this.loadFolderBusyKey(folderId), async () => (await this.resolveAsync(this.api.loadFolderEntries(this.levelId(), folderId, this.requestContext()))).map((entry) => normalizeDocumentLibraryEntry(entry)));
1466
+ const next = new Map(this.folderEntriesCache());
1467
+ next.set(folderKey, entries);
1468
+ this.folderEntriesCache.set(next);
1469
+ const childFolders = entries
1470
+ .filter((entry) => entry.isFolder)
1471
+ .map((entry) => normalizeDocumentLibraryFolder(entry));
1472
+ this.upsertFolders(childFolders);
1473
+ return entries;
1474
+ }
1475
+ async ensureFolderEntries(folderId) {
1476
+ return (this.folderEntriesCache().get(this.toFolderKey(folderId)) ??
1477
+ (await this.fetchFolderEntries(folderId)));
1478
+ }
1479
+ async attachUploadedDocuments(folder, uploads) {
1480
+ const levelDataId = this.levelDataId();
1481
+ if (levelDataId == null || !uploads.length) {
1482
+ return;
1483
+ }
1484
+ try {
1485
+ const attached = [];
1486
+ const failures = [];
1487
+ const uploadFolderCache = new Map();
1488
+ const hydratedTargetFolders = new Set();
1489
+ await this.withBusy(this.uploadBusyKey(folder.id), async () => {
1490
+ for (const upload of uploads) {
1491
+ if (!upload.documentGuid) {
1492
+ continue;
1493
+ }
1494
+ try {
1495
+ const targetFolder = await this.resolveUploadTargetFolder(folder, upload.relativePath, uploadFolderCache);
1496
+ const targetFolderKey = this.toFolderKey(targetFolder.id);
1497
+ if (!hydratedTargetFolders.has(targetFolderKey)) {
1498
+ await this.ensureFolderEntries(targetFolder.id);
1499
+ hydratedTargetFolders.add(targetFolderKey);
1500
+ }
1501
+ const uploaded = normalizeDocumentLibraryFile(await this.resolveAsync(this.api.addDocument(this.levelId(), levelDataId, {
1502
+ folderId: targetFolder.id,
1503
+ documentGuid: upload.documentGuid,
1504
+ }, this.requestContext())));
1505
+ attached.push(uploaded);
1506
+ this.patchFolderEntries(targetFolder.id, (entries) => this.mergeEntriesWithUploadedFiles(entries, [uploaded]));
1507
+ this.actionExecuted.emit({
1508
+ action: 'upload-file',
1509
+ target: uploaded,
1510
+ });
1511
+ }
1512
+ catch {
1513
+ failures.push(upload.fileName);
1514
+ }
1515
+ }
1516
+ });
1517
+ if (attached.length) {
1518
+ this.toast.success(attached.length === 1
1519
+ ? this.translate('messages.fileAttached', {
1520
+ name: attached[0].fileName,
1521
+ })
1522
+ : this.translate('messages.filesAttached', {
1523
+ count: attached.length,
1524
+ }));
1525
+ }
1526
+ if (failures.length) {
1527
+ throw new Error(failures.length === 1
1528
+ ? this.translate('errors.attachFile', {
1529
+ name: failures[0],
1530
+ })
1531
+ : this.translate('errors.attachFiles', {
1532
+ count: failures.length,
1533
+ }));
1534
+ }
1535
+ }
1536
+ catch (error) {
1537
+ this.handleError(error, this.translate('errors.attachUploadedFiles'));
1538
+ }
1539
+ }
1540
+ async createFolderInBackend(parent, folderName) {
1541
+ const trimmedName = folderName.trim();
1542
+ const parentId = parent?.id ?? null;
1543
+ if (parentId != null &&
1544
+ !this.folderEntriesCache().has(this.toFolderKey(parentId))) {
1545
+ await this.ensureFolderEntries(parentId);
1546
+ }
1547
+ const created = await this.withBusy(this.createFolderBusyKey(parentId ?? 'root'), async () => normalizeDocumentLibraryFolder(await this.resolveAsync(this.api.createFolder(this.levelId(), this.levelDataId(), {
1548
+ parentId,
1549
+ folderName: trimmedName,
1550
+ }, this.requestContext()))));
1551
+ this.upsertFolders([created]);
1552
+ this.insertFolderIntoParent(created);
1553
+ this.expandParentForNewFolder(created.parentId);
1554
+ return created;
1555
+ }
1556
+ insertFolderIntoParent(folder) {
1557
+ if (folder.parentId == null) {
1558
+ this.roots.update((folders) => this.sortFolders([...folders, folder]));
1559
+ return;
1560
+ }
1561
+ this.patchFolderEntries(folder.parentId, (entries) => this.sortEntries([...entries, toDocumentLibraryFolderEntry(folder)]));
1562
+ }
1563
+ replaceFolder(folder) {
1564
+ this.upsertFolders([folder]);
1565
+ if (folder.parentId == null) {
1566
+ this.roots.update((folders) => this.sortFolders(folders.map((item) => this.toFolderKey(item.id) === this.toFolderKey(folder.id)
1567
+ ? folder
1568
+ : item)));
1569
+ return;
1570
+ }
1571
+ this.patchFolderEntries(folder.parentId, (entries) => this.sortEntries(entries.map((entry) => entry.isFolder &&
1572
+ this.toFolderKey(entry.id) === this.toFolderKey(folder.id)
1573
+ ? toDocumentLibraryFolderEntry(folder)
1574
+ : entry)));
1575
+ }
1576
+ removeFolderTree(folderId) {
1577
+ const folderKey = this.toFolderKey(folderId);
1578
+ const parentId = this.folderRegistry().get(folderKey)?.parentId ?? null;
1579
+ const folderKeys = new Set(this.collectFolderKeys(folderId));
1580
+ const nextRegistry = new Map(this.folderRegistry());
1581
+ const nextCache = new Map(this.folderEntriesCache());
1582
+ const nextExpanded = new Set(this.expandedFolderKeys());
1583
+ folderKeys.forEach((key) => {
1584
+ nextRegistry.delete(key);
1585
+ nextCache.delete(key);
1586
+ nextExpanded.delete(key);
1587
+ });
1588
+ this.folderRegistry.set(nextRegistry);
1589
+ this.folderEntriesCache.set(nextCache);
1590
+ this.expandedFolderKeys.set(nextExpanded);
1591
+ this.roots.update((folders) => folders.filter((folder) => this.toFolderKey(folder.id) !== this.toFolderKey(folderId)));
1592
+ if (parentId != null) {
1593
+ this.patchFolderEntries(parentId, (entries) => entries.filter((entry) => !(entry.isFolder && this.toFolderKey(entry.id) === folderKey)));
1594
+ }
1595
+ return folderKeys;
1596
+ }
1597
+ patchFolderEntries(folderId, updater) {
1598
+ const folderKey = this.toFolderKey(folderId);
1599
+ const next = new Map(this.folderEntriesCache());
1600
+ const current = next.get(folderKey) ?? [];
1601
+ next.set(folderKey, updater(current));
1602
+ this.folderEntriesCache.set(next);
1603
+ }
1604
+ upsertFolders(folders) {
1605
+ const next = new Map(this.folderRegistry());
1606
+ folders.forEach((folder) => {
1607
+ next.set(this.toFolderKey(folder.id), folder);
1608
+ });
1609
+ this.folderRegistry.set(next);
1610
+ }
1611
+ expandFolderPath(folder) {
1612
+ const next = new Set(this.expandedFolderKeys());
1613
+ let current = folder;
1614
+ while (current) {
1615
+ next.add(this.toFolderKey(current.id));
1616
+ current = this.getParentFolder(current.id);
1617
+ }
1618
+ this.expandedFolderKeys.set(next);
1619
+ }
1620
+ expandParentForNewFolder(parentId) {
1621
+ if (parentId == null) {
1622
+ return;
1623
+ }
1624
+ this.expandedFolderKeys.update((state) => {
1625
+ const next = new Set(state);
1626
+ next.add(this.toFolderKey(parentId));
1627
+ return next;
1628
+ });
1629
+ }
1630
+ syncSelectedFolderEntry(folder) {
1631
+ const selectedEntry = this.selectedEntry();
1632
+ if (!selectedEntry?.isFolder ||
1633
+ this.toFolderKey(selectedEntry.id) !== this.toFolderKey(folder.id)) {
1634
+ return;
1635
+ }
1636
+ const nextEntry = toDocumentLibraryFolderEntry(folder);
1637
+ this.selectedEntry.set(nextEntry);
1638
+ this.selectionChanged.emit({
1639
+ folder: this.selectedFolder(),
1640
+ entry: nextEntry,
1641
+ });
1642
+ }
1643
+ getFolderTrail(folderId) {
1644
+ const trail = [];
1645
+ let currentId = folderId;
1646
+ while (currentId != null) {
1647
+ const folder = this.folderRegistry().get(this.toFolderKey(currentId));
1648
+ if (!folder) {
1649
+ break;
1650
+ }
1651
+ trail.unshift(folder);
1652
+ currentId = folder.parentId;
1653
+ }
1654
+ return trail;
1655
+ }
1656
+ getParentFolder(folderId) {
1657
+ const folder = this.folderRegistry().get(this.toFolderKey(folderId));
1658
+ if (folder?.parentId == null) {
1659
+ return null;
1660
+ }
1661
+ return this.folderRegistry().get(this.toFolderKey(folder.parentId)) ?? null;
1662
+ }
1663
+ async resolveUploadTargetFolder(folder, relativePath, cache) {
1664
+ const segments = this.getUploadFolderSegments(relativePath);
1665
+ if (!segments.length) {
1666
+ return folder;
1667
+ }
1668
+ let currentFolder = folder;
1669
+ for (const segment of segments) {
1670
+ const cacheKey = `${this.toFolderKey(currentFolder.id)}::${segment.toLowerCase()}`;
1671
+ const cachedFolder = cache.get(cacheKey);
1672
+ if (cachedFolder) {
1673
+ currentFolder = cachedFolder;
1674
+ continue;
1675
+ }
1676
+ const nextFolder = await this.findOrCreateChildFolder(currentFolder, segment);
1677
+ cache.set(cacheKey, nextFolder);
1678
+ currentFolder = nextFolder;
1679
+ }
1680
+ return currentFolder;
1681
+ }
1682
+ async findOrCreateChildFolder(parent, folderName) {
1683
+ const childFolders = await this.ensureFolderEntries(parent.id);
1684
+ const existingFolder = childFolders.find((entry) => entry.isFolder && this.folderNameMatches(entry, folderName));
1685
+ if (existingFolder) {
1686
+ const normalizedFolder = normalizeDocumentLibraryFolder(existingFolder);
1687
+ if (!normalizedFolder.isEditableFolder) {
1688
+ throw new Error(this.translate('errors.protectedFolderUpload', {
1689
+ name: this.getFolderLabel(normalizedFolder),
1690
+ }));
1691
+ }
1692
+ return normalizedFolder;
1693
+ }
1694
+ return this.createFolderInBackend(parent, folderName);
1695
+ }
1696
+ folderNameMatches(folder, folderName) {
1697
+ const normalizedTarget = folderName.trim().toLocaleLowerCase();
1698
+ const label = normalizeDocumentLibraryLabel(folder.label);
1699
+ return [label.display, label.en, label.ar]
1700
+ .filter(Boolean)
1701
+ .some((value) => value.trim().toLocaleLowerCase() === normalizedTarget);
1702
+ }
1703
+ getUploadFolderSegments(relativePath) {
1704
+ return relativePath
1705
+ .split(/[\\/]/)
1706
+ .map((segment) => segment.trim())
1707
+ .filter(Boolean)
1708
+ .slice(0, -1);
1709
+ }
1710
+ mergeEntriesWithUploadedFiles(entries, uploaded) {
1711
+ const uploadedIds = new Set(uploaded.map((file) => file.id));
1712
+ return this.sortEntries([
1713
+ ...entries.filter((entry) => entry.isFolder),
1714
+ ...entries.filter((entry) => !entry.isFolder && !uploadedIds.has(entry.id)),
1715
+ ...uploaded,
1716
+ ]);
1717
+ }
1718
+ sortFolders(folders) {
1719
+ return [...folders].sort((left, right) => this.getFolderLabel(left).localeCompare(this.getFolderLabel(right)));
1720
+ }
1721
+ sortEntries(entries) {
1722
+ return [...entries].sort((left, right) => {
1723
+ if (left.isFolder !== right.isFolder) {
1724
+ return left.isFolder ? 1 : -1;
1725
+ }
1726
+ return this.getEntryTitle(left).localeCompare(this.getEntryTitle(right));
1727
+ });
1728
+ }
1729
+ getTargetMenuItems(target) {
1730
+ return this.isEntryTarget(target)
1731
+ ? this.getEntryMenuItems(target)
1732
+ : this.getFolderMenuItems(target);
1733
+ }
1734
+ isEntryTarget(target) {
1735
+ return 'isFolder' in target;
1736
+ }
1737
+ isSelectedFolderInTree(folderKeys) {
1738
+ const selectedFolderId = this.selectedFolderId();
1739
+ return (selectedFolderId != null &&
1740
+ folderKeys.has(this.toFolderKey(selectedFolderId)));
1741
+ }
1742
+ clearSelectionForRemovedFolders(folderKeys) {
1743
+ const selectedEntry = this.selectedEntry();
1744
+ if (!selectedEntry?.isFolder ||
1745
+ !folderKeys.has(this.toFolderKey(selectedEntry.id))) {
1746
+ return;
1747
+ }
1748
+ this.selectedEntry.set(null);
1749
+ this.selectionChanged.emit({
1750
+ folder: this.selectedFolder(),
1751
+ entry: null,
1752
+ });
1753
+ }
1754
+ canRenameFolder(folder) {
1755
+ return this.levelDataId() != null && !!folder.isNameEditable;
1756
+ }
1757
+ collectFolderKeys(folderId) {
1758
+ const targetKey = this.toFolderKey(folderId);
1759
+ const collected = [targetKey];
1760
+ const queue = [targetKey];
1761
+ while (queue.length > 0) {
1762
+ const current = queue.shift();
1763
+ this.folderRegistry().forEach((folder, key) => {
1764
+ if (folder.parentId != null &&
1765
+ this.toFolderKey(folder.parentId) === current &&
1766
+ !collected.includes(key)) {
1767
+ collected.push(key);
1768
+ queue.push(key);
1769
+ }
1770
+ });
1771
+ }
1772
+ return collected;
1773
+ }
1774
+ toFolderKey(folderId) {
1775
+ return String(folderId);
1776
+ }
1777
+ deleteFolderBusyKey(folderId) {
1778
+ return `delete-folder:${this.toFolderKey(folderId)}`;
1779
+ }
1780
+ deleteFileBusyKey(fileId) {
1781
+ return `delete-file:${fileId}`;
1782
+ }
1783
+ uploadBusyKey(folderId) {
1784
+ return `upload-file:${this.toFolderKey(folderId)}`;
1785
+ }
1786
+ async withBusy(key, task) {
1787
+ this.busyKeys.update((state) => {
1788
+ const next = new Set(state);
1789
+ next.add(key);
1790
+ return next;
1791
+ });
1792
+ try {
1793
+ return await task();
1794
+ }
1795
+ finally {
1796
+ this.busyKeys.update((state) => {
1797
+ const next = new Set(state);
1798
+ next.delete(key);
1799
+ return next;
1800
+ });
1801
+ }
1802
+ }
1803
+ async resolveAsync(value) {
1804
+ if (isObservable(value)) {
1805
+ return firstValueFrom(value);
1806
+ }
1807
+ return Promise.resolve(value);
1808
+ }
1809
+ handleError(error, fallbackMessage) {
1810
+ const message = error instanceof Error
1811
+ ? error.message
1812
+ : typeof error === 'string'
1813
+ ? error
1814
+ : fallbackMessage;
1815
+ this.error.set(message);
1816
+ this.toast.error(message);
1817
+ this.errored.emit(message);
1818
+ }
1819
+ translate(key, params) {
1820
+ this.activeLanguage();
1821
+ return this.transloco.translate(`document-library.${key}`, params);
1822
+ }
1823
+ showPopover(popover, event) {
1824
+ if (!popover) {
1825
+ return;
1826
+ }
1827
+ popover.hide();
1828
+ setTimeout(() => popover.show(event), 0);
1829
+ }
1830
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibrary, deps: [], target: i0.ɵɵFactoryTarget.Component });
1831
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: DocumentLibrary, isStandalone: true, selector: "mt-document-library", inputs: { levelId: { classPropertyName: "levelId", publicName: "levelId", isSignal: true, isRequired: true, transformFunction: null }, levelDataId: { classPropertyName: "levelDataId", publicName: "levelDataId", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, lang: { classPropertyName: "lang", publicName: "lang", isSignal: true, isRequired: false, transformFunction: null }, requestContext: { classPropertyName: "requestContext", publicName: "requestContext", isSignal: true, isRequired: false, transformFunction: null }, uploadEndPoint: { classPropertyName: "uploadEndPoint", publicName: "uploadEndPoint", isSignal: true, isRequired: false, transformFunction: null }, emptyTitle: { classPropertyName: "emptyTitle", publicName: "emptyTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", errored: "errored", selectionChanged: "selectionChanged", actionExecuted: "actionExecuted" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "nameCellTpl", first: true, predicate: ["nameCellTpl"], descendants: true, isSignal: true }, { propertyName: "updatedCellTpl", first: true, predicate: ["updatedCellTpl"], descendants: true, isSignal: true }, { propertyName: "actionsCellTpl", first: true, predicate: ["actionsCellTpl"], descendants: true, isSignal: true }, { propertyName: "addPopover", first: true, predicate: ["addPopover"], descendants: true, isSignal: true }, { propertyName: "folderPopover", first: true, predicate: ["folderPopover"], descendants: true, isSignal: true }, { propertyName: "contextPopover", first: true, predicate: ["contextPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'document-library'\">\r\n <div class=\"flex h-full min-h-[42rem] flex-col gap-4\">\r\n <div class=\"grid gap-4 xl:grid-cols-[19rem_minmax(0,1fr)]\">\r\n <mt-card class=\"h-full\">\r\n <ng-template #headless>\r\n <div class=\"flex h-full flex-col\">\r\n <div class=\"border-b border-surface px-5 py-5\">\r\n <div class=\"flex items-start justify-between gap-3\">\r\n <div>\r\n <div class=\"text-base font-semibold\">\r\n {{ resolvedTitle() }}\r\n </div>\r\n </div>\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"\r\n t('aria.refresh', { title: resolvedTitle() })\r\n \"\r\n [disabled]=\"isBusy('roots')\"\r\n (click)=\"reload()\"\r\n >\r\n <mt-avatar\r\n icon=\"arrow.refresh-cw-01\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n </div>\r\n <div class=\"mt-3\">\r\n <mt-text-field\r\n [ngModel]=\"railSearch()\"\r\n (ngModelChange)=\"railSearch.set($event)\"\r\n icon=\"general.search-lg\"\r\n [placeholder]=\"t('searchFolders')\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex-1 overflow-y-auto px-2.5 py-2.5\">\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n getRailItemClass(selectedFolderId() === null) +\n ' mb-0.5 flex items-center gap-2.5 px-2.5 py-2'\r\n \"\r\n (click)=\"goToRoot()\"\r\n >\r\n <mt-avatar\r\n class=\"[&_.p-avatar]:h-9 [&_.p-avatar]:w-9 [&_.p-avatar]:text-sm\"\r\n icon=\"general.home-05\"\r\n size=\"small\"\r\n [shape]=\"'square'\"\r\n [style]=\"getHomeAvatarStyle()\"\r\n />\r\n <span class=\"truncate\">{{ resolvedTitle() }}</span>\r\n </button>\r\n\r\n <ng-template #folderNodeTpl let-folder let-depth=\"depth\">\r\n <div class=\"mt-px\">\r\n <div\r\n class=\"flex items-center gap-1.5 rounded-lg\"\r\n [style.padding-inline-start.rem]=\"0.1 + depth * 0.72\"\r\n >\r\n <button\r\n type=\"button\"\r\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-color transition hover:bg-surface\"\r\n [class.invisible]=\"\r\n !folderHasChildFolders(folder.id) &&\r\n !hasLoadedChildren(folder.id)\r\n \"\r\n (click)=\"toggleExpanded(folder)\"\r\n >\r\n <mt-icon\r\n class=\"text-[0.85rem]\"\r\n [icon]=\"\r\n isExpanded(folder.id)\r\n ? 'arrow.chevron-down'\r\n : 'arrow.chevron-right'\r\n \"\r\n />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n getRailItemClass(isSelectedFolder(folder.id)) +\r\n ' flex min-w-0 flex-1 items-center gap-2.5 px-2.5 py-2'\r\n \"\r\n (click)=\"onRailFolderClick(folder)\"\r\n (contextmenu)=\"openContextMenu($event, folder)\"\r\n >\r\n <mt-avatar\r\n class=\"shrink-0 [&_.p-avatar]:h-9 [&_.p-avatar]:w-9 [&_.p-avatar]:text-sm\"\r\n size=\"small\"\r\n [icon]=\"getFolderIcon(folder)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(folder)\"\r\n />\r\n <span class=\"min-w-0 flex-1 truncate\">\r\n {{ getFolderLabel(folder) }}\r\n </span>\r\n </button>\r\n </div>\r\n\r\n @if (isExpanded(folder.id)) {\r\n <div class=\"space-y-px\">\r\n @for (\r\n child of foldersForRail(folder.id);\r\n track child.id\r\n ) {\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n folderNodeTpl;\r\n context: { $implicit: child, depth: depth + 1 }\r\n \"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n @for (folder of visibleRoots(); track folder.id) {\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n folderNodeTpl;\r\n context: { $implicit: folder, depth: 0 }\r\n \"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n\r\n <div class=\"flex min-w-0 flex-col gap-4\">\r\n <mt-card>\r\n <div class=\"flex flex-wrap items-center justify-between gap-3\">\r\n <div class=\"min-w-0 flex-1\">\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbs()\"\r\n (onItemClick)=\"onBreadcrumbClick($event)\"\r\n />\r\n <div class=\"mt-2 flex items-start gap-3\">\r\n <mt-avatar\r\n [icon]=\"\r\n selectedFolder()\r\n ? getFolderIcon(selectedFolder()!)\r\n : 'general.home-05'\r\n \"\r\n size=\"normal\"\r\n [shape]=\"'square'\"\r\n [style]=\"\r\n selectedFolder()\r\n ? getFolderAvatarStyle(selectedFolder()!)\r\n : getHomeAvatarStyle()\r\n \"\r\n />\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-lg font-semibold\">\r\n {{ currentFolderTitle() }}\r\n </div>\r\n <div class=\"truncate text-sm text-muted-color\">\r\n {{ currentFolderSubtitle() }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex flex-wrap items-center gap-2 self-start\">\r\n @if (hasVisibleActionItems(addMenuItems())) {\r\n <mt-button\r\n [label]=\"t('actions.add')\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n (onClick)=\"openAddPopover($event)\"\r\n />\r\n <p-popover\r\n #addPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: { items: addMenuItems(), popover: addPopover }\r\n \"\r\n />\r\n </p-popover>\r\n }\r\n\r\n @if (\r\n selectedFolder() &&\r\n hasVisibleActionItems(currentFolderMenuItems())\r\n ) {\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"t('aria.folderActions')\"\r\n (click)=\"openFolderPopover($event)\"\r\n >\r\n <mt-avatar\r\n icon=\"general.dots-horizontal\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n <p-popover\r\n #folderPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: {\r\n items: currentFolderMenuItems(),\r\n popover: folderPopover,\r\n }\r\n \"\r\n />\r\n </p-popover>\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (error()) {\r\n <div\r\n class=\"mt-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\r\n >\r\n {{ error() }}\r\n </div>\r\n }\r\n </mt-card>\r\n\r\n <mt-card class=\"min-w-0\">\r\n <div class=\"grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem]\">\r\n <div class=\"min-w-0\">\r\n <mt-table\r\n [columns]=\"columns()\"\r\n [data]=\"tableRows()\"\r\n [loading]=\"\r\n selectedFolderId() === null\n ? isBusy('roots')\r\n : isBusy(loadFolderBusyKey(selectedFolderId()!))\r\n \"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"onRowClick($event)\"\r\n >\r\n <ng-template #empty>\r\n <div class=\"flex flex-col items-center gap-3 py-10\">\r\n <mt-avatar\r\n icon=\"file.folder-question\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getHomeAvatarStyle()\"\r\n />\r\n <div class=\"text-base font-semibold\">\r\n {{ resolvedEmptyTitle() }}\r\n </div>\r\n <div class=\"max-w-md text-center text-sm text-muted-color\">\r\n {{ resolvedEmptyDescription() }}\r\n </div>\r\n @if (\r\n canCreateFolderInSelection() || canUploadIntoCurrentFolder()\r\n ) {\r\n <div class=\"flex flex-wrap justify-center gap-2\">\r\n @if (canCreateFolderInSelection()) {\r\n <mt-button\r\n [label]=\"t('actions.newFolder')\"\r\n icon=\"file.folder-plus\"\r\n (onClick)=\"openCreateFolderDialog(selectedFolder())\"\r\n />\r\n }\r\n @if (canUploadIntoCurrentFolder()) {\r\n <mt-button\r\n [label]=\"t('actions.uploadFile')\"\r\n icon=\"general.upload-01\"\r\n [outlined]=\"true\"\r\n (onClick)=\"openUploadDialog('file')\"\r\n />\r\n <mt-button\r\n [label]=\"t('actions.uploadFolder')\"\r\n icon=\"file.folder\"\r\n [outlined]=\"true\"\r\n (onClick)=\"openUploadDialog('folder')\"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n </mt-table>\r\n </div>\r\n\r\n <div\r\n class=\"min-w-0 rounded-2xl border border-surface bg-surface/30 p-4\"\r\n >\r\n <div class=\"text-sm font-semibold\">\r\n {{ t(\"selection.title\") }}\r\n </div>\r\n\r\n @if (selectedEntry(); as entry) {\r\n <div class=\"mt-4 space-y-4\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (entry.isFolder) {\r\n <mt-avatar\r\n [icon]=\"getFolderIcon(entry)\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(entry)\"\r\n />\r\n } @else {\r\n <mt-avatar\r\n [icon]=\"getFileIcon(entry)\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFileAvatarStyle(entry)\"\r\n />\r\n }\r\n\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-base font-semibold\">\r\n {{ getEntryTitle(entry) }}\r\n </div>\r\n <div class=\"truncate text-sm text-muted-color\">\r\n {{\r\n entry.isFolder\r\n ? t(\"selection.folder\")\r\n : t(\"selection.file\")\r\n }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (entry.isFolder) {\r\n <div class=\"grid gap-3 text-sm\">\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.permissions\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{\r\n entry.isEditableFolder\r\n ? t(\"selection.editableArea\")\r\n : t(\"selection.systemManaged\")\r\n }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.rename\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{\r\n entry.isNameEditable\r\n ? t(\"selection.allowed\")\r\n : t(\"selection.protected\")\r\n }}\r\n </div>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"grid gap-3 text-sm\">\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.type\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium uppercase\">\r\n {{ getFileExtension(entry) }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.size\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{ formatFileSize(entry.size) }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.owner\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{ entry.createdBy || t(\"owner.unknown\") }}\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"mt-4 text-sm text-muted-color\">\r\n {{ t(\"selection.inspectHint\") }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <ng-template #actionListTpl let-items=\"items\" let-popover=\"popover\">\r\n <div class=\"flex flex-col gap-0.5 p-0.5\">\r\n @for (item of getVisibleActionItems(items); track trackActionItem(item, $index)) {\r\n <button\r\n type=\"button\"\r\n [class]=\"getActionItemButtonClass(item)\"\r\n (click)=\"executePopoverItem(item, popover)\"\r\n >\r\n @if (item.icon) {\r\n <mt-avatar\r\n class=\"[&_.p-avatar]:h-8 [&_.p-avatar]:w-8 [&_.p-avatar]:text-xs\"\r\n [icon]=\"item.icon\"\r\n [shape]=\"'square'\"\r\n [style]=\"getActionItemAvatarStyle(item)\"\r\n />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n </button>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #nameCellTpl let-row>\r\n <div\r\n class=\"flex min-w-0 items-center gap-3\"\r\n (contextmenu)=\"openContextMenu($event, row.entry)\"\r\n >\r\n @if (row.entry.isFolder) {\r\n <mt-avatar\r\n class=\"shrink-0\"\r\n [icon]=\"getFolderIcon(row.entry)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(row.entry)\"\r\n />\r\n } @else {\r\n <mt-avatar\r\n class=\"shrink-0\"\r\n [icon]=\"getFileIcon(row.entry)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFileAvatarStyle(row.entry)\"\r\n />\r\n }\r\n\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-sm font-semibold\">\r\n {{ row.name }}\r\n </div>\r\n @if (row.entry.isFolder) {\r\n <div class=\"truncate text-xs text-muted-color\">\r\n {{\r\n row.entry.isEditableFolder\r\n ? t(\"selection.manualFolder\")\r\n : t(\"selection.protectedFolder\")\r\n }}\r\n </div>\r\n } @else {\r\n <div class=\"truncate text-xs text-muted-color uppercase\">\r\n {{ getFileExtension(row.entry) }} -\r\n {{ formatFileSize(row.entry.size) }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #updatedCellTpl let-row>\r\n @if (row.entry.isFolder) {\r\n <span class=\"text-sm text-muted-color\">-</span>\r\n } @else {\r\n <div class=\"text-sm\">\r\n {{ row.updatedAt | date: \"dd MMM, y\" }}\r\n </div>\r\n }\r\n </ng-template>\r\n\r\n <ng-template #actionsCellTpl let-row>\r\n @if (hasVisibleActionItems(getEntryMenuItems(row.entry))) {\r\n <div\r\n class=\"flex w-full items-center justify-center\"\r\n data-row-click-ignore=\"true\"\r\n >\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"t('aria.rowActions')\"\r\n (click)=\"rowPopover.toggle($event)\"\r\n >\r\n <mt-avatar\r\n icon=\"general.dots-vertical\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n <p-popover\r\n #rowPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: {\r\n items: getEntryMenuItems(row.entry),\r\n popover: rowPopover,\r\n }\r\n \"\r\n />\r\n </p-popover>\r\n </div>\r\n }\r\n </ng-template>\r\n\r\n <p-popover\r\n #contextPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: { items: contextMenuItems(), popover: contextPopover }\r\n \"\r\n />\r\n </p-popover>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "showFilters", "loading", "updating", "lazy", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "exportable", "exportFilename", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "paginatorPosition", "pageSize", "currentPage", "first", "filterTerm"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }] });
1832
+ }
1833
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: DocumentLibrary, decorators: [{
1834
+ type: Component,
1835
+ args: [{ selector: 'mt-document-library', standalone: true, host: {
1836
+ class: 'block',
1837
+ }, imports: [
1838
+ CommonModule,
1839
+ FormsModule,
1840
+ Avatar,
1841
+ Breadcrumb,
1842
+ Button,
1843
+ Card,
1844
+ DatePipe,
1845
+ Icon,
1846
+ Popover,
1847
+ Table,
1848
+ TextField,
1849
+ TranslocoDirective,
1850
+ ], template: "<ng-container *transloco=\"let t; prefix: 'document-library'\">\r\n <div class=\"flex h-full min-h-[42rem] flex-col gap-4\">\r\n <div class=\"grid gap-4 xl:grid-cols-[19rem_minmax(0,1fr)]\">\r\n <mt-card class=\"h-full\">\r\n <ng-template #headless>\r\n <div class=\"flex h-full flex-col\">\r\n <div class=\"border-b border-surface px-5 py-5\">\r\n <div class=\"flex items-start justify-between gap-3\">\r\n <div>\r\n <div class=\"text-base font-semibold\">\r\n {{ resolvedTitle() }}\r\n </div>\r\n </div>\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"\r\n t('aria.refresh', { title: resolvedTitle() })\r\n \"\r\n [disabled]=\"isBusy('roots')\"\r\n (click)=\"reload()\"\r\n >\r\n <mt-avatar\r\n icon=\"arrow.refresh-cw-01\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n </div>\r\n <div class=\"mt-3\">\r\n <mt-text-field\r\n [ngModel]=\"railSearch()\"\r\n (ngModelChange)=\"railSearch.set($event)\"\r\n icon=\"general.search-lg\"\r\n [placeholder]=\"t('searchFolders')\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex-1 overflow-y-auto px-2.5 py-2.5\">\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n getRailItemClass(selectedFolderId() === null) +\n ' mb-0.5 flex items-center gap-2.5 px-2.5 py-2'\r\n \"\r\n (click)=\"goToRoot()\"\r\n >\r\n <mt-avatar\r\n class=\"[&_.p-avatar]:h-9 [&_.p-avatar]:w-9 [&_.p-avatar]:text-sm\"\r\n icon=\"general.home-05\"\r\n size=\"small\"\r\n [shape]=\"'square'\"\r\n [style]=\"getHomeAvatarStyle()\"\r\n />\r\n <span class=\"truncate\">{{ resolvedTitle() }}</span>\r\n </button>\r\n\r\n <ng-template #folderNodeTpl let-folder let-depth=\"depth\">\r\n <div class=\"mt-px\">\r\n <div\r\n class=\"flex items-center gap-1.5 rounded-lg\"\r\n [style.padding-inline-start.rem]=\"0.1 + depth * 0.72\"\r\n >\r\n <button\r\n type=\"button\"\r\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-color transition hover:bg-surface\"\r\n [class.invisible]=\"\r\n !folderHasChildFolders(folder.id) &&\r\n !hasLoadedChildren(folder.id)\r\n \"\r\n (click)=\"toggleExpanded(folder)\"\r\n >\r\n <mt-icon\r\n class=\"text-[0.85rem]\"\r\n [icon]=\"\r\n isExpanded(folder.id)\r\n ? 'arrow.chevron-down'\r\n : 'arrow.chevron-right'\r\n \"\r\n />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n getRailItemClass(isSelectedFolder(folder.id)) +\r\n ' flex min-w-0 flex-1 items-center gap-2.5 px-2.5 py-2'\r\n \"\r\n (click)=\"onRailFolderClick(folder)\"\r\n (contextmenu)=\"openContextMenu($event, folder)\"\r\n >\r\n <mt-avatar\r\n class=\"shrink-0 [&_.p-avatar]:h-9 [&_.p-avatar]:w-9 [&_.p-avatar]:text-sm\"\r\n size=\"small\"\r\n [icon]=\"getFolderIcon(folder)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(folder)\"\r\n />\r\n <span class=\"min-w-0 flex-1 truncate\">\r\n {{ getFolderLabel(folder) }}\r\n </span>\r\n </button>\r\n </div>\r\n\r\n @if (isExpanded(folder.id)) {\r\n <div class=\"space-y-px\">\r\n @for (\r\n child of foldersForRail(folder.id);\r\n track child.id\r\n ) {\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n folderNodeTpl;\r\n context: { $implicit: child, depth: depth + 1 }\r\n \"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n @for (folder of visibleRoots(); track folder.id) {\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n folderNodeTpl;\r\n context: { $implicit: folder, depth: 0 }\r\n \"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n\r\n <div class=\"flex min-w-0 flex-col gap-4\">\r\n <mt-card>\r\n <div class=\"flex flex-wrap items-center justify-between gap-3\">\r\n <div class=\"min-w-0 flex-1\">\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbs()\"\r\n (onItemClick)=\"onBreadcrumbClick($event)\"\r\n />\r\n <div class=\"mt-2 flex items-start gap-3\">\r\n <mt-avatar\r\n [icon]=\"\r\n selectedFolder()\r\n ? getFolderIcon(selectedFolder()!)\r\n : 'general.home-05'\r\n \"\r\n size=\"normal\"\r\n [shape]=\"'square'\"\r\n [style]=\"\r\n selectedFolder()\r\n ? getFolderAvatarStyle(selectedFolder()!)\r\n : getHomeAvatarStyle()\r\n \"\r\n />\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-lg font-semibold\">\r\n {{ currentFolderTitle() }}\r\n </div>\r\n <div class=\"truncate text-sm text-muted-color\">\r\n {{ currentFolderSubtitle() }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex flex-wrap items-center gap-2 self-start\">\r\n @if (hasVisibleActionItems(addMenuItems())) {\r\n <mt-button\r\n [label]=\"t('actions.add')\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n (onClick)=\"openAddPopover($event)\"\r\n />\r\n <p-popover\r\n #addPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: { items: addMenuItems(), popover: addPopover }\r\n \"\r\n />\r\n </p-popover>\r\n }\r\n\r\n @if (\r\n selectedFolder() &&\r\n hasVisibleActionItems(currentFolderMenuItems())\r\n ) {\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"t('aria.folderActions')\"\r\n (click)=\"openFolderPopover($event)\"\r\n >\r\n <mt-avatar\r\n icon=\"general.dots-horizontal\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n <p-popover\r\n #folderPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: {\r\n items: currentFolderMenuItems(),\r\n popover: folderPopover,\r\n }\r\n \"\r\n />\r\n </p-popover>\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (error()) {\r\n <div\r\n class=\"mt-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\r\n >\r\n {{ error() }}\r\n </div>\r\n }\r\n </mt-card>\r\n\r\n <mt-card class=\"min-w-0\">\r\n <div class=\"grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem]\">\r\n <div class=\"min-w-0\">\r\n <mt-table\r\n [columns]=\"columns()\"\r\n [data]=\"tableRows()\"\r\n [loading]=\"\r\n selectedFolderId() === null\n ? isBusy('roots')\r\n : isBusy(loadFolderBusyKey(selectedFolderId()!))\r\n \"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"onRowClick($event)\"\r\n >\r\n <ng-template #empty>\r\n <div class=\"flex flex-col items-center gap-3 py-10\">\r\n <mt-avatar\r\n icon=\"file.folder-question\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getHomeAvatarStyle()\"\r\n />\r\n <div class=\"text-base font-semibold\">\r\n {{ resolvedEmptyTitle() }}\r\n </div>\r\n <div class=\"max-w-md text-center text-sm text-muted-color\">\r\n {{ resolvedEmptyDescription() }}\r\n </div>\r\n @if (\r\n canCreateFolderInSelection() || canUploadIntoCurrentFolder()\r\n ) {\r\n <div class=\"flex flex-wrap justify-center gap-2\">\r\n @if (canCreateFolderInSelection()) {\r\n <mt-button\r\n [label]=\"t('actions.newFolder')\"\r\n icon=\"file.folder-plus\"\r\n (onClick)=\"openCreateFolderDialog(selectedFolder())\"\r\n />\r\n }\r\n @if (canUploadIntoCurrentFolder()) {\r\n <mt-button\r\n [label]=\"t('actions.uploadFile')\"\r\n icon=\"general.upload-01\"\r\n [outlined]=\"true\"\r\n (onClick)=\"openUploadDialog('file')\"\r\n />\r\n <mt-button\r\n [label]=\"t('actions.uploadFolder')\"\r\n icon=\"file.folder\"\r\n [outlined]=\"true\"\r\n (onClick)=\"openUploadDialog('folder')\"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n </mt-table>\r\n </div>\r\n\r\n <div\r\n class=\"min-w-0 rounded-2xl border border-surface bg-surface/30 p-4\"\r\n >\r\n <div class=\"text-sm font-semibold\">\r\n {{ t(\"selection.title\") }}\r\n </div>\r\n\r\n @if (selectedEntry(); as entry) {\r\n <div class=\"mt-4 space-y-4\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (entry.isFolder) {\r\n <mt-avatar\r\n [icon]=\"getFolderIcon(entry)\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(entry)\"\r\n />\r\n } @else {\r\n <mt-avatar\r\n [icon]=\"getFileIcon(entry)\"\r\n size=\"large\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFileAvatarStyle(entry)\"\r\n />\r\n }\r\n\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-base font-semibold\">\r\n {{ getEntryTitle(entry) }}\r\n </div>\r\n <div class=\"truncate text-sm text-muted-color\">\r\n {{\r\n entry.isFolder\r\n ? t(\"selection.folder\")\r\n : t(\"selection.file\")\r\n }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (entry.isFolder) {\r\n <div class=\"grid gap-3 text-sm\">\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.permissions\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{\r\n entry.isEditableFolder\r\n ? t(\"selection.editableArea\")\r\n : t(\"selection.systemManaged\")\r\n }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.rename\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{\r\n entry.isNameEditable\r\n ? t(\"selection.allowed\")\r\n : t(\"selection.protected\")\r\n }}\r\n </div>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"grid gap-3 text-sm\">\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.type\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium uppercase\">\r\n {{ getFileExtension(entry) }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.size\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{ formatFileSize(entry.size) }}\r\n </div>\r\n </div>\r\n <div\r\n class=\"rounded-2xl border border-surface bg-content px-3 py-3\"\r\n >\r\n <div\r\n class=\"text-xs uppercase tracking-wide text-muted-color\"\r\n >\r\n {{ t(\"selection.owner\") }}\r\n </div>\r\n <div class=\"mt-1 font-medium\">\r\n {{ entry.createdBy || t(\"owner.unknown\") }}\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"mt-4 text-sm text-muted-color\">\r\n {{ t(\"selection.inspectHint\") }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <ng-template #actionListTpl let-items=\"items\" let-popover=\"popover\">\r\n <div class=\"flex flex-col gap-0.5 p-0.5\">\r\n @for (item of getVisibleActionItems(items); track trackActionItem(item, $index)) {\r\n <button\r\n type=\"button\"\r\n [class]=\"getActionItemButtonClass(item)\"\r\n (click)=\"executePopoverItem(item, popover)\"\r\n >\r\n @if (item.icon) {\r\n <mt-avatar\r\n class=\"[&_.p-avatar]:h-8 [&_.p-avatar]:w-8 [&_.p-avatar]:text-xs\"\r\n [icon]=\"item.icon\"\r\n [shape]=\"'square'\"\r\n [style]=\"getActionItemAvatarStyle(item)\"\r\n />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n </button>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #nameCellTpl let-row>\r\n <div\r\n class=\"flex min-w-0 items-center gap-3\"\r\n (contextmenu)=\"openContextMenu($event, row.entry)\"\r\n >\r\n @if (row.entry.isFolder) {\r\n <mt-avatar\r\n class=\"shrink-0\"\r\n [icon]=\"getFolderIcon(row.entry)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFolderAvatarStyle(row.entry)\"\r\n />\r\n } @else {\r\n <mt-avatar\r\n class=\"shrink-0\"\r\n [icon]=\"getFileIcon(row.entry)\"\r\n [shape]=\"'square'\"\r\n [style]=\"getFileAvatarStyle(row.entry)\"\r\n />\r\n }\r\n\r\n <div class=\"min-w-0\">\r\n <div class=\"truncate text-sm font-semibold\">\r\n {{ row.name }}\r\n </div>\r\n @if (row.entry.isFolder) {\r\n <div class=\"truncate text-xs text-muted-color\">\r\n {{\r\n row.entry.isEditableFolder\r\n ? t(\"selection.manualFolder\")\r\n : t(\"selection.protectedFolder\")\r\n }}\r\n </div>\r\n } @else {\r\n <div class=\"truncate text-xs text-muted-color uppercase\">\r\n {{ getFileExtension(row.entry) }} -\r\n {{ formatFileSize(row.entry.size) }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #updatedCellTpl let-row>\r\n @if (row.entry.isFolder) {\r\n <span class=\"text-sm text-muted-color\">-</span>\r\n } @else {\r\n <div class=\"text-sm\">\r\n {{ row.updatedAt | date: \"dd MMM, y\" }}\r\n </div>\r\n }\r\n </ng-template>\r\n\r\n <ng-template #actionsCellTpl let-row>\r\n @if (hasVisibleActionItems(getEntryMenuItems(row.entry))) {\r\n <div\r\n class=\"flex w-full items-center justify-center\"\r\n data-row-click-ignore=\"true\"\r\n >\r\n <button\r\n type=\"button\"\r\n [class]=\"getIconButtonClass()\"\r\n [attr.aria-label]=\"t('aria.rowActions')\"\r\n (click)=\"rowPopover.toggle($event)\"\r\n >\r\n <mt-avatar\r\n icon=\"general.dots-vertical\"\r\n [shape]=\"'circle'\"\r\n [style]=\"getActionAvatarStyle()\"\r\n />\r\n </button>\r\n <p-popover\r\n #rowPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: {\r\n items: getEntryMenuItems(row.entry),\r\n popover: rowPopover,\r\n }\r\n \"\r\n />\r\n </p-popover>\r\n </div>\r\n }\r\n </ng-template>\r\n\r\n <p-popover\r\n #contextPopover\r\n [style]=\"getActionPopoverStyle()\"\r\n appendTo=\"body\"\r\n >\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n actionListTpl;\r\n context: { items: contextMenuItems(), popover: contextPopover }\r\n \"\r\n />\r\n </p-popover>\r\n </div>\r\n</ng-container>\r\n" }]
1851
+ }], ctorParameters: () => [], propDecorators: { levelId: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelId", required: true }] }], levelDataId: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelDataId", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "lang", required: false }] }], requestContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestContext", required: false }] }], uploadEndPoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadEndPoint", required: false }] }], emptyTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTitle", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], errored: [{ type: i0.Output, args: ["errored"] }], selectionChanged: [{ type: i0.Output, args: ["selectionChanged"] }], actionExecuted: [{ type: i0.Output, args: ["actionExecuted"] }], nameCellTpl: [{ type: i0.ViewChild, args: ['nameCellTpl', { isSignal: true }] }], updatedCellTpl: [{ type: i0.ViewChild, args: ['updatedCellTpl', { isSignal: true }] }], actionsCellTpl: [{ type: i0.ViewChild, args: ['actionsCellTpl', { isSignal: true }] }], addPopover: [{ type: i0.ViewChild, args: ['addPopover', { isSignal: true }] }], folderPopover: [{ type: i0.ViewChild, args: ['folderPopover', { isSignal: true }] }], contextPopover: [{ type: i0.ViewChild, args: ['contextPopover', { isSignal: true }] }] } });
1852
+
1853
+ function isDocumentLibraryFolderEntry(entry) {
1854
+ return !!entry && 'isFolder' in entry && entry.isFolder;
1855
+ }
1856
+ function isDocumentLibraryFileEntry(entry) {
1857
+ return !!entry && !entry.isFolder;
1858
+ }
1859
+
1860
+ /**
1861
+ * Generated bundle index. Do not edit.
1862
+ */
1863
+
1864
+ export { DocumentLibrary, isDocumentLibraryFileEntry, isDocumentLibraryFolderEntry };
1865
+ //# sourceMappingURL=masterteam-document-library.mjs.map