@mohamedatia/fly-design-system 1.4.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mohamedatia-fly-design-system.mjs +908 -21
- package/fesm2022/mohamedatia-fly-design-system.mjs.map +1 -1
- package/package.json +6 -5
- package/scss/_fly-theme.scss +2 -2
- package/scss/_theme-auto.scss +4 -3
- package/scss/_theme-dark.scss +77 -4
- package/scss/_theme-light.scss +11 -3
- package/scss/_theme-spatial-vars.scss +66 -0
- package/scss/_theme-spatial.scss +22 -0
- package/types/mohamedatia-fly-design-system.d.ts +292 -12
- package/types/mohamedatia-fly-design-system.d.ts.map +1 -1
- package/scss/_theme-dark-vars.scss +0 -57
- package/scss/_theme-transparent.scss +0 -41
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, Pipe, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component,
|
|
2
|
+
import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, Output, Input, viewChild, ViewEncapsulation, model } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
|
3
4
|
import { Router } from '@angular/router';
|
|
4
|
-
import { of } from 'rxjs';
|
|
5
|
+
import { of, ReplaySubject } from 'rxjs';
|
|
6
|
+
import * as i1 from '@angular/forms';
|
|
7
|
+
import { FormsModule } from '@angular/forms';
|
|
8
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
|
+
import { switchMap } from 'rxjs/operators';
|
|
10
|
+
import { HttpClient, HttpEventType } from '@angular/common/http';
|
|
11
|
+
import Cropper from 'cropperjs';
|
|
5
12
|
|
|
6
13
|
const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
|
|
7
14
|
|
|
@@ -225,21 +232,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
225
232
|
args: [{ providedIn: 'root' }]
|
|
226
233
|
}] });
|
|
227
234
|
|
|
235
|
+
/** Single source of truth for persisted / API theme strings. */
|
|
236
|
+
const FLY_THEME_MODE_IDS = ['light', 'spatial', 'dark'];
|
|
237
|
+
/** Factory default: `dark` (opaque dark / `html.dark-theme`); shell i18n `settings.theme.dark`. Keep in sync with `UserSettings` defaults. */
|
|
238
|
+
const DEFAULT_FLY_THEME_MODE = 'dark';
|
|
239
|
+
/** Coerce unknown values (localStorage, API) to a valid theme mode. */
|
|
240
|
+
function normalizeFlyTheme(value) {
|
|
241
|
+
if (typeof value === 'string' && FLY_THEME_MODE_IDS.includes(value)) {
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
return DEFAULT_FLY_THEME_MODE;
|
|
245
|
+
}
|
|
228
246
|
/**
|
|
229
|
-
* Applies `html.light-theme` / `html.
|
|
247
|
+
* Applies `html.light-theme` / `html.spatial-theme` / `html.dark-theme` for DS SCSS.
|
|
230
248
|
* Shell and standalone Business Apps use the same service (federation singleton when shared).
|
|
231
249
|
*/
|
|
232
250
|
class FlyThemeService {
|
|
233
|
-
|
|
251
|
+
platformId = inject(PLATFORM_ID);
|
|
252
|
+
theme = signal(DEFAULT_FLY_THEME_MODE, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
234
253
|
applyTheme(mode) {
|
|
235
254
|
this.theme.set(mode);
|
|
255
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
236
258
|
const html = document.documentElement;
|
|
237
|
-
html.classList.remove('
|
|
238
|
-
if (mode === '
|
|
239
|
-
html.classList.add('
|
|
259
|
+
html.classList.remove('light-theme', 'spatial-theme', 'dark-theme');
|
|
260
|
+
if (mode === 'spatial') {
|
|
261
|
+
html.classList.add('spatial-theme');
|
|
240
262
|
}
|
|
241
|
-
else if (mode === '
|
|
242
|
-
html.classList.add('
|
|
263
|
+
else if (mode === 'dark') {
|
|
264
|
+
html.classList.add('dark-theme');
|
|
243
265
|
}
|
|
244
266
|
else {
|
|
245
267
|
html.classList.add('light-theme');
|
|
@@ -260,6 +282,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
260
282
|
* on the shell's internal implementation.
|
|
261
283
|
*/
|
|
262
284
|
class WindowManagerService {
|
|
285
|
+
/**
|
|
286
|
+
* Shell: increments per-window depth and shows content block UI.
|
|
287
|
+
* Standalone / no host: no-op.
|
|
288
|
+
*/
|
|
289
|
+
beginContentUiBlock(_windowId, _messageKey) { }
|
|
290
|
+
/** Shell: decrements depth; clears block when depth reaches 0. Standalone: no-op. */
|
|
291
|
+
endContentUiBlock(_windowId) { }
|
|
263
292
|
}
|
|
264
293
|
/**
|
|
265
294
|
* No-op fallback implementation used when running a Business App in
|
|
@@ -348,6 +377,13 @@ class MockAuthService {
|
|
|
348
377
|
hasPendingTwoFactor;
|
|
349
378
|
/** Always false in mock mode — auth is pre-populated synchronously. */
|
|
350
379
|
initializing;
|
|
380
|
+
/**
|
|
381
|
+
* Shell and guards may read this flag; real auth uses `localStorage` `fly_had_session`.
|
|
382
|
+
* Default false — override in the app’s `auth.service.mock.ts` if needed.
|
|
383
|
+
*/
|
|
384
|
+
get hadPriorSession() {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
351
387
|
constructor() {
|
|
352
388
|
// All signal/computed calls are inside the constructor body so Angular's
|
|
353
389
|
// injection context and ngDevMode are fully set up before they execute.
|
|
@@ -417,32 +453,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
417
453
|
|
|
418
454
|
class ContextMenuComponent {
|
|
419
455
|
menuEl;
|
|
456
|
+
doc = inject(DOCUMENT);
|
|
457
|
+
hostEl = inject((ElementRef));
|
|
420
458
|
x = input.required(...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
|
|
421
459
|
y = input.required(...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
422
460
|
sections = input.required(...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
|
|
423
461
|
action = output();
|
|
424
462
|
closed = output();
|
|
425
|
-
menuWidth = signal(
|
|
426
|
-
menuHeight = signal(
|
|
463
|
+
menuWidth = signal(0, ...(ngDevMode ? [{ debugName: "menuWidth" }] : /* istanbul ignore next */ []));
|
|
464
|
+
menuHeight = signal(0, ...(ngDevMode ? [{ debugName: "menuHeight" }] : /* istanbul ignore next */ []));
|
|
427
465
|
previouslyFocused = null;
|
|
466
|
+
// Resolved position: clamp to viewport so the menu never overflows an edge.
|
|
467
|
+
// menuWidth/Height start at 0 so the first render places the menu at (x,y)
|
|
468
|
+
// with no clamping; ngAfterViewInit measures the real size and updates the
|
|
469
|
+
// signals, which re-evaluates this computed and repositions correctly.
|
|
428
470
|
clampedPos = computed(() => {
|
|
429
|
-
const vw =
|
|
430
|
-
const vh =
|
|
471
|
+
const vw = this.doc.defaultView?.innerWidth ?? 0;
|
|
472
|
+
const vh = this.doc.defaultView?.innerHeight ?? 0;
|
|
473
|
+
const w = this.menuWidth();
|
|
474
|
+
const h = this.menuHeight();
|
|
475
|
+
const x = this.x();
|
|
476
|
+
const y = this.y();
|
|
431
477
|
return {
|
|
432
|
-
|
|
433
|
-
|
|
478
|
+
left: w > 0 ? Math.min(x, vw - w - 8) : x,
|
|
479
|
+
top: h > 0 ? Math.min(y, vh - h - 8) : y,
|
|
434
480
|
};
|
|
435
481
|
}, ...(ngDevMode ? [{ debugName: "clampedPos" }] : /* istanbul ignore next */ []));
|
|
436
482
|
ngAfterViewInit() {
|
|
483
|
+
// Portal: move host to <body> so position:fixed resolves against the true
|
|
484
|
+
// viewport, not against any transformed/backdrop-filtered ancestor window.
|
|
485
|
+
this.doc.body.appendChild(this.hostEl.nativeElement);
|
|
486
|
+
// Measure after portal move so offsetWidth/Height are accurate.
|
|
437
487
|
const el = this.menuEl?.nativeElement;
|
|
438
488
|
if (el) {
|
|
439
489
|
this.menuWidth.set(el.offsetWidth);
|
|
440
490
|
this.menuHeight.set(el.offsetHeight);
|
|
441
491
|
}
|
|
442
|
-
// Store focus origin and move focus to first menu item
|
|
443
492
|
this.previouslyFocused = document.activeElement;
|
|
444
493
|
this.focusItem(0);
|
|
445
494
|
}
|
|
495
|
+
ngOnDestroy() {
|
|
496
|
+
const el = this.hostEl.nativeElement;
|
|
497
|
+
if (el.parentNode === this.doc.body) {
|
|
498
|
+
this.doc.body.removeChild(el);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
446
501
|
onAction(id) {
|
|
447
502
|
this.action.emit(id);
|
|
448
503
|
this.close();
|
|
@@ -501,11 +556,11 @@ class ContextMenuComponent {
|
|
|
501
556
|
this.previouslyFocused = null;
|
|
502
557
|
}
|
|
503
558
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
504
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ContextMenuComponent, isStandalone: true, selector: "fly-context-menu", inputs: { x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: true, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { action: "action", closed: "closed" }, host: { listeners: { "document:mousedown": "onClickOutside($event)", "document:keydown.escape": "onEscape()", "document:contextmenu": "onContextMenu($event)", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["contextMenu"], descendants: true }], ngImport: i0, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().
|
|
559
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ContextMenuComponent, isStandalone: true, selector: "fly-context-menu", inputs: { x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: true, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { action: "action", closed: "closed" }, host: { listeners: { "document:mousedown": "onClickOutside($event)", "document:keydown.escape": "onEscape()", "document:contextmenu": "onContextMenu($event)", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["contextMenu"], descendants: true }], ngImport: i0, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
505
560
|
}
|
|
506
561
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ContextMenuComponent, decorators: [{
|
|
507
562
|
type: Component,
|
|
508
|
-
args: [{ selector: 'fly-context-menu', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().
|
|
563
|
+
args: [{ selector: 'fly-context-menu', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"] }]
|
|
509
564
|
}], propDecorators: { menuEl: [{
|
|
510
565
|
type: ViewChild,
|
|
511
566
|
args: ['contextMenu']
|
|
@@ -680,11 +735,835 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
680
735
|
args: ['document:keydown.escape']
|
|
681
736
|
}] } });
|
|
682
737
|
|
|
738
|
+
/** Default permission levels (file-style ACL). Hosts can override via `permissionLevels`. */
|
|
739
|
+
const SHARE_PANEL_DEFAULT_FILE_LEVELS = [
|
|
740
|
+
{ value: 'View', labelKey: 'files.share.level_view' },
|
|
741
|
+
{ value: 'Download', labelKey: 'files.share.level_download' },
|
|
742
|
+
{ value: 'Edit', labelKey: 'files.share.level_edit' },
|
|
743
|
+
{ value: 'FullControl', labelKey: 'files.share.level_full' },
|
|
744
|
+
];
|
|
745
|
+
class SharePanelComponent {
|
|
746
|
+
/** Resource title shown in the header */
|
|
747
|
+
targetName = '';
|
|
748
|
+
/** i18n key for panel title */
|
|
749
|
+
titleKey = 'files.share.title';
|
|
750
|
+
/** Load current shares / permissions (host resolves display names when possible). */
|
|
751
|
+
loadPermissions;
|
|
752
|
+
searchUsers;
|
|
753
|
+
/**
|
|
754
|
+
* Load OU hierarchy for sharing. `chartId` is null for the default/official tree;
|
|
755
|
+
* non-null for an alternative chart.
|
|
756
|
+
*/
|
|
757
|
+
loadOuTree;
|
|
758
|
+
/** Chart selector options; first entry should be the default chart (`id: null`). */
|
|
759
|
+
loadChartOptions;
|
|
760
|
+
/**
|
|
761
|
+
* Labels from the default (official) OU tree for stable display names on OU grants
|
|
762
|
+
* when the picker shows an alternative chart.
|
|
763
|
+
*/
|
|
764
|
+
loadOuLabelMap;
|
|
765
|
+
grantToUser;
|
|
766
|
+
grantToOu;
|
|
767
|
+
updatePermission;
|
|
768
|
+
revokePermission;
|
|
769
|
+
/** When true, show “apply to children” (e.g. folders). */
|
|
770
|
+
showApplyToChildren = false;
|
|
771
|
+
/** Override level dropdown options (e.g. notes: View / Edit only). */
|
|
772
|
+
permissionLevels;
|
|
773
|
+
/** i18n key for wildcard app grant label */
|
|
774
|
+
everyoneLabelKey = 'files.share.everyone';
|
|
775
|
+
close = new EventEmitter();
|
|
776
|
+
destroyRef = inject(DestroyRef);
|
|
777
|
+
i18n = inject(I18nService);
|
|
778
|
+
permissions = signal([], ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []));
|
|
779
|
+
loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
780
|
+
searchQuery = '';
|
|
781
|
+
newLevel = '';
|
|
782
|
+
applyToChildren = false;
|
|
783
|
+
searchResults = signal([], ...(ngDevMode ? [{ debugName: "searchResults" }] : /* istanbul ignore next */ []));
|
|
784
|
+
ouTree = signal([], ...(ngDevMode ? [{ debugName: "ouTree" }] : /* istanbul ignore next */ []));
|
|
785
|
+
showOuPicker = signal(false, ...(ngDevMode ? [{ debugName: "showOuPicker" }] : /* istanbul ignore next */ []));
|
|
786
|
+
chartOptions = signal([], ...(ngDevMode ? [{ debugName: "chartOptions" }] : /* istanbul ignore next */ []));
|
|
787
|
+
selectedOrgChartId = signal(null, ...(ngDevMode ? [{ debugName: "selectedOrgChartId" }] : /* istanbul ignore next */ []));
|
|
788
|
+
/** Default-tree id → display name for permission rows (OU grants). */
|
|
789
|
+
ouLabelById = signal({}, ...(ngDevMode ? [{ debugName: "ouLabelById" }] : /* istanbul ignore next */ []));
|
|
790
|
+
searchTimeout = null;
|
|
791
|
+
/** Emits after chart options load; further emissions on user chart changes (no initial null). */
|
|
792
|
+
selectedChartId$ = new ReplaySubject(1);
|
|
793
|
+
ngOnInit() {
|
|
794
|
+
const levels = this.levelOptions;
|
|
795
|
+
this.newLevel = levels[0]?.value ?? 'View';
|
|
796
|
+
this.refreshPermissions();
|
|
797
|
+
this.loadOuLabelMap()
|
|
798
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
799
|
+
.subscribe({
|
|
800
|
+
next: (m) => this.ouLabelById.set(m),
|
|
801
|
+
error: () => this.ouLabelById.set({}),
|
|
802
|
+
});
|
|
803
|
+
this.selectedChartId$
|
|
804
|
+
.pipe(switchMap((id) => this.loadOuTree(id)), takeUntilDestroyed(this.destroyRef))
|
|
805
|
+
.subscribe({
|
|
806
|
+
next: (tree) => this.ouTree.set(tree),
|
|
807
|
+
error: () => this.ouTree.set([]),
|
|
808
|
+
});
|
|
809
|
+
this.loadChartOptions()
|
|
810
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
811
|
+
.subscribe({
|
|
812
|
+
next: (opts) => {
|
|
813
|
+
this.chartOptions.set(opts);
|
|
814
|
+
const first = opts[0];
|
|
815
|
+
const initial = first ? first.id : null;
|
|
816
|
+
this.selectedOrgChartId.set(initial);
|
|
817
|
+
this.selectedChartId$.next(initial);
|
|
818
|
+
},
|
|
819
|
+
error: () => {
|
|
820
|
+
this.chartOptions.set([]);
|
|
821
|
+
this.selectedOrgChartId.set(null);
|
|
822
|
+
this.selectedChartId$.next(null);
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
onOrgChartSelectChange(chartId) {
|
|
827
|
+
this.selectedOrgChartId.set(chartId);
|
|
828
|
+
this.selectedChartId$.next(chartId);
|
|
829
|
+
}
|
|
830
|
+
get levelOptions() {
|
|
831
|
+
const p = this.permissionLevels;
|
|
832
|
+
return p && p.length > 0 ? p : SHARE_PANEL_DEFAULT_FILE_LEVELS;
|
|
833
|
+
}
|
|
834
|
+
refreshPermissions() {
|
|
835
|
+
this.loading.set(true);
|
|
836
|
+
this.loadPermissions()
|
|
837
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
838
|
+
.subscribe({
|
|
839
|
+
next: (perms) => {
|
|
840
|
+
this.permissions.set(perms);
|
|
841
|
+
this.loading.set(false);
|
|
842
|
+
},
|
|
843
|
+
error: () => {
|
|
844
|
+
this.permissions.set([]);
|
|
845
|
+
this.loading.set(false);
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
onSearchInput() {
|
|
850
|
+
if (this.searchTimeout)
|
|
851
|
+
clearTimeout(this.searchTimeout);
|
|
852
|
+
const query = this.searchQuery.trim();
|
|
853
|
+
if (query.length < 2) {
|
|
854
|
+
this.searchResults.set([]);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this.searchTimeout = setTimeout(() => {
|
|
858
|
+
this.searchUsers(query)
|
|
859
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
860
|
+
.subscribe({
|
|
861
|
+
next: (items) => this.searchResults.set(items),
|
|
862
|
+
error: () => this.searchResults.set([]),
|
|
863
|
+
});
|
|
864
|
+
}, 300);
|
|
865
|
+
}
|
|
866
|
+
onGrantToUser(user) {
|
|
867
|
+
this.searchResults.set([]);
|
|
868
|
+
this.searchQuery = '';
|
|
869
|
+
this.grantToUser(user.id, this.newLevel, this.applyToChildren)
|
|
870
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
871
|
+
.subscribe({ next: () => this.refreshPermissions() });
|
|
872
|
+
}
|
|
873
|
+
onGrantToOu(ou) {
|
|
874
|
+
this.grantToOu(ou.id, this.newLevel, this.applyToChildren)
|
|
875
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
876
|
+
.subscribe({ next: () => this.refreshPermissions() });
|
|
877
|
+
}
|
|
878
|
+
onUpdateLevel(perm, level) {
|
|
879
|
+
this.updatePermission(perm.id, level)
|
|
880
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
881
|
+
.subscribe({ next: () => this.refreshPermissions() });
|
|
882
|
+
}
|
|
883
|
+
onRevokePermission(perm) {
|
|
884
|
+
this.revokePermission(perm.id)
|
|
885
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
886
|
+
.subscribe({ next: () => this.refreshPermissions() });
|
|
887
|
+
}
|
|
888
|
+
getPermIcon(perm) {
|
|
889
|
+
if (perm.grantedToUserId)
|
|
890
|
+
return 'pi pi-user';
|
|
891
|
+
if (perm.grantedToOuId)
|
|
892
|
+
return 'pi pi-sitemap';
|
|
893
|
+
if (perm.grantedToAppId)
|
|
894
|
+
return 'pi pi-globe';
|
|
895
|
+
return 'pi pi-question';
|
|
896
|
+
}
|
|
897
|
+
/** Logical start padding for OU hierarchy (roots align with section padding). */
|
|
898
|
+
ouIndentStartPx(ou) {
|
|
899
|
+
const d = ou.depth ?? 0;
|
|
900
|
+
return 12 + d * 18;
|
|
901
|
+
}
|
|
902
|
+
getPermLabel(perm) {
|
|
903
|
+
if (perm.displayName?.trim())
|
|
904
|
+
return perm.displayName.trim();
|
|
905
|
+
if (perm.grantedToUserId) {
|
|
906
|
+
return `${perm.grantedToUserId.substring(0, 8)}…`;
|
|
907
|
+
}
|
|
908
|
+
if (perm.grantedToOuId) {
|
|
909
|
+
const id = perm.grantedToOuId;
|
|
910
|
+
const fromDefault = this.ouLabelById()[id];
|
|
911
|
+
if (fromDefault)
|
|
912
|
+
return fromDefault;
|
|
913
|
+
const ou = this.ouTree().find((o) => o.id === id);
|
|
914
|
+
if (ou)
|
|
915
|
+
return ou.displayName;
|
|
916
|
+
return `${id.substring(0, 8)}…`;
|
|
917
|
+
}
|
|
918
|
+
if (perm.grantedToAppId === '*')
|
|
919
|
+
return this.i18n.t(this.everyoneLabelKey);
|
|
920
|
+
return perm.grantedToAppId ?? '';
|
|
921
|
+
}
|
|
922
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
923
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: SharePanelComponent, isStandalone: true, selector: "fly-share-panel", inputs: { targetName: "targetName", titleKey: "titleKey", loadPermissions: "loadPermissions", searchUsers: "searchUsers", loadOuTree: "loadOuTree", loadChartOptions: "loadChartOptions", loadOuLabelMap: "loadOuLabelMap", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (chartOptions().length > 0) {\n <div class=\"fac-chart-row\">\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\n 'files.share.org_chart' | translate\n }}</label>\n <select\n id=\"fac-org-chart-select\"\n class=\"fac-select\"\n [ngModel]=\"selectedOrgChartId()\"\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\n", styles: [".fac-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:2000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fac-panel{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:12px;width:600px;max-width:min(600px,96vw);max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 16px 48px #0006}.fac-header{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-header h3{margin:0;font-size:16px;font-weight:600}.fac-target-name{flex:1;font-size:13px;color:var(--text-color-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:16px;padding:4px}.fac-close:hover{color:var(--text-color)}.fac-add-section{padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-search-row{display:flex;gap:8px}.fac-chart-row{display:flex;flex-direction:column;gap:6px;margin-top:12px}.fac-chart-row .fac-select{width:100%}.fac-input{flex:1;background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fac-input:focus{border-color:var(--primary-color, #e8732a)}.fac-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 10px;color:inherit;font-size:13px;outline:none}.fac-select.sm{padding:4px 8px;font-size:12px}.fac-search-results{margin-top:8px;max-height:150px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-search-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-search-item:hover{background:var(--surface-hover)}.fac-search-item i{color:var(--text-color-secondary)}.fac-email{color:var(--text-color-secondary);font-size:12px;margin-inline-start:auto}.fac-ou-section{margin-top:10px;max-height:220px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 12px;padding-inline-start:12px;cursor:pointer;font-size:13px;border-radius:6px}.fac-ou-item:hover{background:var(--surface-hover)}.fac-ou-item i{color:var(--text-color-secondary)}.fac-section-label{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-color-secondary);letter-spacing:.5px;margin-bottom:8px}.fac-toggle-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.fac-link{background:none;border:none;color:var(--primary-color, #e8732a);cursor:pointer;font-size:12px;padding:0}.fac-link:hover{text-decoration:underline}.fac-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fac-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fac-perms-section{flex:1;overflow-y:auto;padding:16px 20px}.fac-loading,.fac-empty{text-align:center;padding:20px;color:var(--text-color-secondary);font-size:13px}.fac-perm-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);gap:8px}.fac-perm-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.fac-perm-info i{color:var(--text-color-secondary);flex-shrink:0}.fac-perm-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-badge{font-size:10px;padding:2px 6px;border-radius:8px;flex-shrink:0}.fac-badge.inherited{background:#ffffff1a;color:var(--text-color-secondary)}.fac-perm-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.fac-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:12px}.fac-icon-btn:hover{background:#ffffff24}.fac-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fac-icon-btn:disabled{opacity:.4;cursor:default}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { 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: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
924
|
+
}
|
|
925
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, decorators: [{
|
|
926
|
+
type: Component,
|
|
927
|
+
args: [{ selector: 'fly-share-panel', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (chartOptions().length > 0) {\n <div class=\"fac-chart-row\">\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\n 'files.share.org_chart' | translate\n }}</label>\n <select\n id=\"fac-org-chart-select\"\n class=\"fac-select\"\n [ngModel]=\"selectedOrgChartId()\"\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\n", styles: [".fac-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:2000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fac-panel{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:12px;width:600px;max-width:min(600px,96vw);max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 16px 48px #0006}.fac-header{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-header h3{margin:0;font-size:16px;font-weight:600}.fac-target-name{flex:1;font-size:13px;color:var(--text-color-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:16px;padding:4px}.fac-close:hover{color:var(--text-color)}.fac-add-section{padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-search-row{display:flex;gap:8px}.fac-chart-row{display:flex;flex-direction:column;gap:6px;margin-top:12px}.fac-chart-row .fac-select{width:100%}.fac-input{flex:1;background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fac-input:focus{border-color:var(--primary-color, #e8732a)}.fac-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 10px;color:inherit;font-size:13px;outline:none}.fac-select.sm{padding:4px 8px;font-size:12px}.fac-search-results{margin-top:8px;max-height:150px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-search-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-search-item:hover{background:var(--surface-hover)}.fac-search-item i{color:var(--text-color-secondary)}.fac-email{color:var(--text-color-secondary);font-size:12px;margin-inline-start:auto}.fac-ou-section{margin-top:10px;max-height:220px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 12px;padding-inline-start:12px;cursor:pointer;font-size:13px;border-radius:6px}.fac-ou-item:hover{background:var(--surface-hover)}.fac-ou-item i{color:var(--text-color-secondary)}.fac-section-label{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-color-secondary);letter-spacing:.5px;margin-bottom:8px}.fac-toggle-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.fac-link{background:none;border:none;color:var(--primary-color, #e8732a);cursor:pointer;font-size:12px;padding:0}.fac-link:hover{text-decoration:underline}.fac-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fac-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fac-perms-section{flex:1;overflow-y:auto;padding:16px 20px}.fac-loading,.fac-empty{text-align:center;padding:20px;color:var(--text-color-secondary);font-size:13px}.fac-perm-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);gap:8px}.fac-perm-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.fac-perm-info i{color:var(--text-color-secondary);flex-shrink:0}.fac-perm-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-badge{font-size:10px;padding:2px 6px;border-radius:8px;flex-shrink:0}.fac-badge.inherited{background:#ffffff1a;color:var(--text-color-secondary)}.fac-perm-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.fac-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:12px}.fac-icon-btn:hover{background:#ffffff24}.fac-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fac-icon-btn:disabled{opacity:.4;cursor:default}\n"] }]
|
|
928
|
+
}], propDecorators: { targetName: [{
|
|
929
|
+
type: Input
|
|
930
|
+
}], titleKey: [{
|
|
931
|
+
type: Input
|
|
932
|
+
}], loadPermissions: [{
|
|
933
|
+
type: Input,
|
|
934
|
+
args: [{ required: true }]
|
|
935
|
+
}], searchUsers: [{
|
|
936
|
+
type: Input,
|
|
937
|
+
args: [{ required: true }]
|
|
938
|
+
}], loadOuTree: [{
|
|
939
|
+
type: Input,
|
|
940
|
+
args: [{ required: true }]
|
|
941
|
+
}], loadChartOptions: [{
|
|
942
|
+
type: Input,
|
|
943
|
+
args: [{ required: true }]
|
|
944
|
+
}], loadOuLabelMap: [{
|
|
945
|
+
type: Input,
|
|
946
|
+
args: [{ required: true }]
|
|
947
|
+
}], grantToUser: [{
|
|
948
|
+
type: Input,
|
|
949
|
+
args: [{ required: true }]
|
|
950
|
+
}], grantToOu: [{
|
|
951
|
+
type: Input,
|
|
952
|
+
args: [{ required: true }]
|
|
953
|
+
}], updatePermission: [{
|
|
954
|
+
type: Input,
|
|
955
|
+
args: [{ required: true }]
|
|
956
|
+
}], revokePermission: [{
|
|
957
|
+
type: Input,
|
|
958
|
+
args: [{ required: true }]
|
|
959
|
+
}], showApplyToChildren: [{
|
|
960
|
+
type: Input
|
|
961
|
+
}], permissionLevels: [{
|
|
962
|
+
type: Input
|
|
963
|
+
}], everyoneLabelKey: [{
|
|
964
|
+
type: Input
|
|
965
|
+
}], close: [{
|
|
966
|
+
type: Output
|
|
967
|
+
}] } });
|
|
968
|
+
|
|
969
|
+
/** Full-bleed loading overlay for window content (shell) or embedded hosts. */
|
|
970
|
+
class FlyBlockUiComponent {
|
|
971
|
+
/** When false, the overlay is not rendered (host may use @if instead). */
|
|
972
|
+
active = input.required(...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
973
|
+
/** i18n key for status text; empty uses `common.loading`. */
|
|
974
|
+
messageKey = input('', ...(ngDevMode ? [{ debugName: "messageKey" }] : /* istanbul ignore next */ []));
|
|
975
|
+
resolvedMessageKey = computed(() => {
|
|
976
|
+
const k = this.messageKey()?.trim();
|
|
977
|
+
return k && k.length > 0 ? k : 'common.loading';
|
|
978
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
|
|
979
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
980
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);backdrop-filter:blur(6px) saturate(120%);-webkit-backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
981
|
+
}
|
|
982
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
|
|
983
|
+
type: Component,
|
|
984
|
+
args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);backdrop-filter:blur(6px) saturate(120%);-webkit-backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
|
|
985
|
+
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Image upload component with built-in cropperjs cropping modal.
|
|
989
|
+
*
|
|
990
|
+
* Usage:
|
|
991
|
+
* ```html
|
|
992
|
+
* <fly-image-upload
|
|
993
|
+
* [aspectRatio]="16/9"
|
|
994
|
+
* [currentImageId]="trend.coverImageId"
|
|
995
|
+
* sourceApp="circles"
|
|
996
|
+
* sourceEntityType="trend"
|
|
997
|
+
* (uploaded)="onImageUploaded($event)"
|
|
998
|
+
* (removed)="onImageRemoved()"
|
|
999
|
+
* />
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
class FlyImageUploadComponent {
|
|
1003
|
+
http = inject(HttpClient);
|
|
1004
|
+
// ── Inputs ──
|
|
1005
|
+
aspectRatio = input(16 / 9, ...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
|
|
1006
|
+
maxSizeBytes = input(5 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxSizeBytes" }] : /* istanbul ignore next */ []));
|
|
1007
|
+
currentImageId = input(null, ...(ngDevMode ? [{ debugName: "currentImageId" }] : /* istanbul ignore next */ []));
|
|
1008
|
+
sourceApp = input('unknown', ...(ngDevMode ? [{ debugName: "sourceApp" }] : /* istanbul ignore next */ []));
|
|
1009
|
+
sourceEntityType = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityType" }] : /* istanbul ignore next */ []));
|
|
1010
|
+
sourceEntityId = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityId" }] : /* istanbul ignore next */ []));
|
|
1011
|
+
// ── Outputs ──
|
|
1012
|
+
uploaded = output();
|
|
1013
|
+
removed = output();
|
|
1014
|
+
// ── State ──
|
|
1015
|
+
uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
|
|
1016
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1017
|
+
showCropper = signal(false, ...(ngDevMode ? [{ debugName: "showCropper" }] : /* istanbul ignore next */ []));
|
|
1018
|
+
rawImageUrl = signal('', ...(ngDevMode ? [{ debugName: "rawImageUrl" }] : /* istanbul ignore next */ []));
|
|
1019
|
+
localBlob = signal(null, ...(ngDevMode ? [{ debugName: "localBlob" }] : /* istanbul ignore next */ []));
|
|
1020
|
+
fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
|
|
1021
|
+
cropImage = viewChild('cropImage', ...(ngDevMode ? [{ debugName: "cropImage" }] : /* istanbul ignore next */ []));
|
|
1022
|
+
cropper = null;
|
|
1023
|
+
selectedFile = null;
|
|
1024
|
+
previewUrl = computed(() => {
|
|
1025
|
+
if (this.localBlob())
|
|
1026
|
+
return this.localBlob();
|
|
1027
|
+
const id = this.currentImageId();
|
|
1028
|
+
return id ? `/api/files/${id}/download` : null;
|
|
1029
|
+
}, ...(ngDevMode ? [{ debugName: "previewUrl" }] : /* istanbul ignore next */ []));
|
|
1030
|
+
sizeHint = computed(() => {
|
|
1031
|
+
const mb = this.maxSizeBytes() / (1024 * 1024);
|
|
1032
|
+
return `Max ${mb}MB`;
|
|
1033
|
+
}, ...(ngDevMode ? [{ debugName: "sizeHint" }] : /* istanbul ignore next */ []));
|
|
1034
|
+
triggerFileInput() {
|
|
1035
|
+
this.fileInput()?.nativeElement.click();
|
|
1036
|
+
}
|
|
1037
|
+
onDragOver(e) {
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
e.stopPropagation();
|
|
1040
|
+
}
|
|
1041
|
+
onDrop(e) {
|
|
1042
|
+
e.preventDefault();
|
|
1043
|
+
e.stopPropagation();
|
|
1044
|
+
const file = e.dataTransfer?.files?.[0];
|
|
1045
|
+
if (file)
|
|
1046
|
+
this.processFile(file);
|
|
1047
|
+
}
|
|
1048
|
+
onFileSelected(e) {
|
|
1049
|
+
const input = e.target;
|
|
1050
|
+
const file = input.files?.[0];
|
|
1051
|
+
if (file)
|
|
1052
|
+
this.processFile(file);
|
|
1053
|
+
input.value = '';
|
|
1054
|
+
}
|
|
1055
|
+
removeImage() {
|
|
1056
|
+
this.localBlob.set(null);
|
|
1057
|
+
this.error.set(null);
|
|
1058
|
+
this.removed.emit();
|
|
1059
|
+
}
|
|
1060
|
+
cancelCrop() {
|
|
1061
|
+
this.showCropper.set(false);
|
|
1062
|
+
this.destroyCropper();
|
|
1063
|
+
if (this.rawImageUrl())
|
|
1064
|
+
URL.revokeObjectURL(this.rawImageUrl());
|
|
1065
|
+
this.rawImageUrl.set('');
|
|
1066
|
+
}
|
|
1067
|
+
applyCrop() {
|
|
1068
|
+
if (!this.cropper)
|
|
1069
|
+
return;
|
|
1070
|
+
const canvas = this.cropper.getCroppedCanvas({ maxWidth: 1920, maxHeight: 1080 });
|
|
1071
|
+
this.showCropper.set(false);
|
|
1072
|
+
this.destroyCropper();
|
|
1073
|
+
if (this.rawImageUrl())
|
|
1074
|
+
URL.revokeObjectURL(this.rawImageUrl());
|
|
1075
|
+
this.rawImageUrl.set('');
|
|
1076
|
+
canvas.toBlob((blob) => {
|
|
1077
|
+
if (!blob)
|
|
1078
|
+
return;
|
|
1079
|
+
const fileName = this.selectedFile?.name ?? 'cropped-image.jpg';
|
|
1080
|
+
const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
|
|
1081
|
+
this.uploadFile(file);
|
|
1082
|
+
}, 'image/jpeg', 0.9);
|
|
1083
|
+
}
|
|
1084
|
+
processFile(file) {
|
|
1085
|
+
this.error.set(null);
|
|
1086
|
+
if (!file.type.startsWith('image/')) {
|
|
1087
|
+
this.error.set('Only image files are allowed');
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (file.size > this.maxSizeBytes()) {
|
|
1091
|
+
const mb = this.maxSizeBytes() / (1024 * 1024);
|
|
1092
|
+
this.error.set(`File exceeds ${mb}MB limit`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
this.selectedFile = file;
|
|
1096
|
+
const url = URL.createObjectURL(file);
|
|
1097
|
+
this.rawImageUrl.set(url);
|
|
1098
|
+
this.showCropper.set(true);
|
|
1099
|
+
// Wait for next tick so cropImage element is in DOM
|
|
1100
|
+
setTimeout(() => this.initCropper(), 0);
|
|
1101
|
+
}
|
|
1102
|
+
initCropper() {
|
|
1103
|
+
this.destroyCropper();
|
|
1104
|
+
const imgEl = this.cropImage()?.nativeElement;
|
|
1105
|
+
if (!imgEl)
|
|
1106
|
+
return;
|
|
1107
|
+
this.cropper = new Cropper(imgEl, {
|
|
1108
|
+
aspectRatio: this.aspectRatio(),
|
|
1109
|
+
viewMode: 1,
|
|
1110
|
+
autoCropArea: 0.9,
|
|
1111
|
+
responsive: true,
|
|
1112
|
+
background: false,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
destroyCropper() {
|
|
1116
|
+
this.cropper?.destroy();
|
|
1117
|
+
this.cropper = null;
|
|
1118
|
+
}
|
|
1119
|
+
uploadFile(file) {
|
|
1120
|
+
this.uploading.set(true);
|
|
1121
|
+
this.error.set(null);
|
|
1122
|
+
const formData = new FormData();
|
|
1123
|
+
formData.append('file', file, file.name);
|
|
1124
|
+
formData.append('sourceApp', this.sourceApp());
|
|
1125
|
+
if (this.sourceEntityType())
|
|
1126
|
+
formData.append('sourceEntityType', this.sourceEntityType());
|
|
1127
|
+
if (this.sourceEntityId())
|
|
1128
|
+
formData.append('sourceEntityId', this.sourceEntityId());
|
|
1129
|
+
this.http.post('/api/files/upload', formData).subscribe({
|
|
1130
|
+
next: (res) => {
|
|
1131
|
+
this.uploading.set(false);
|
|
1132
|
+
const info = res.data ?? res;
|
|
1133
|
+
this.localBlob.set(URL.createObjectURL(file));
|
|
1134
|
+
this.uploaded.emit(info);
|
|
1135
|
+
},
|
|
1136
|
+
error: (err) => {
|
|
1137
|
+
this.uploading.set(false);
|
|
1138
|
+
this.error.set(err?.error?.message ?? 'Upload failed');
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1143
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyImageUploadComponent, isStandalone: true, selector: "fly-image-upload", inputs: { aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, maxSizeBytes: { classPropertyName: "maxSizeBytes", publicName: "maxSizeBytes", isSignal: true, isRequired: false, transformFunction: null }, currentImageId: { classPropertyName: "currentImageId", publicName: "currentImageId", isSignal: true, isRequired: false, transformFunction: null }, sourceApp: { classPropertyName: "sourceApp", publicName: "sourceApp", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityType: { classPropertyName: "sourceEntityType", publicName: "sourceEntityType", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityId: { classPropertyName: "sourceEntityId", publicName: "sourceEntityId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploaded: "uploaded", removed: "removed" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "cropImage", first: true, predicate: ["cropImage"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1144
|
+
<div class="fly-image-upload">
|
|
1145
|
+
@if (previewUrl()) {
|
|
1146
|
+
<div class="fly-image-upload__preview">
|
|
1147
|
+
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
1148
|
+
<div class="fly-image-upload__overlay">
|
|
1149
|
+
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
1150
|
+
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
1151
|
+
</button>
|
|
1152
|
+
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
1153
|
+
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
1154
|
+
</button>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
} @else {
|
|
1158
|
+
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
1159
|
+
@if (uploading()) {
|
|
1160
|
+
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
1161
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
1162
|
+
} @else {
|
|
1163
|
+
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
1164
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
1165
|
+
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
1166
|
+
}
|
|
1167
|
+
</button>
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
@if (error()) {
|
|
1171
|
+
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
1175
|
+
|
|
1176
|
+
@if (showCropper()) {
|
|
1177
|
+
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
1178
|
+
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
1179
|
+
<div class="fly-image-upload__crop-header">
|
|
1180
|
+
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
1181
|
+
</div>
|
|
1182
|
+
<div class="fly-image-upload__crop-body">
|
|
1183
|
+
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
1184
|
+
</div>
|
|
1185
|
+
<div class="fly-image-upload__crop-footer">
|
|
1186
|
+
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
1187
|
+
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
}
|
|
1192
|
+
</div>
|
|
1193
|
+
`, isInline: true, styles: [".cropper-container{-webkit-touch-callout:none;direction:ltr;font-size:0;line-height:0;position:relative;touch-action:none;-webkit-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{inset:0;position:absolute}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}\n", ".fly-image-upload{display:flex;flex-direction:column;gap:6px}.fly-image-upload__hidden{display:none}.fly-image-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:12px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s;min-height:120px}.fly-image-upload__dropzone:hover{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__icon{font-size:28px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-image-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__preview{position:relative;border-radius:12px;overflow:hidden}.fly-image-upload__img{display:block;width:100%;max-height:280px;object-fit:cover;border-radius:12px}.fly-image-upload__overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:12px;background:#00000059;opacity:0;transition:opacity .15s}.fly-image-upload__preview:hover .fly-image-upload__overlay{opacity:1}.fly-image-upload__action-btn{width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;background:#ffffffe6;color:var(--label-primary, #1f2937);font-size:14px;transition:background .15s}.fly-image-upload__action-btn:hover{background:#fff}.fly-image-upload__action-btn--danger:hover{background:#fee2e2;color:#dc2626}.fly-image-upload__error{font-size:12px;color:var(--system-red, #ef4444)}.fly-image-upload__crop-backdrop{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0000004d}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden}.fly-image-upload__crop-body img{display:block;max-width:100%}.fly-image-upload__crop-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--separator-primary, #e5e7eb)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1194
|
+
}
|
|
1195
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, decorators: [{
|
|
1196
|
+
type: Component,
|
|
1197
|
+
args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
1198
|
+
<div class="fly-image-upload">
|
|
1199
|
+
@if (previewUrl()) {
|
|
1200
|
+
<div class="fly-image-upload__preview">
|
|
1201
|
+
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
1202
|
+
<div class="fly-image-upload__overlay">
|
|
1203
|
+
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
1204
|
+
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
1205
|
+
</button>
|
|
1206
|
+
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
1207
|
+
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
1208
|
+
</button>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
} @else {
|
|
1212
|
+
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
1213
|
+
@if (uploading()) {
|
|
1214
|
+
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
1215
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
1216
|
+
} @else {
|
|
1217
|
+
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
1218
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
1219
|
+
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
1220
|
+
}
|
|
1221
|
+
</button>
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
@if (error()) {
|
|
1225
|
+
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
1229
|
+
|
|
1230
|
+
@if (showCropper()) {
|
|
1231
|
+
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
1232
|
+
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
1233
|
+
<div class="fly-image-upload__crop-header">
|
|
1234
|
+
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
1235
|
+
</div>
|
|
1236
|
+
<div class="fly-image-upload__crop-body">
|
|
1237
|
+
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
1238
|
+
</div>
|
|
1239
|
+
<div class="fly-image-upload__crop-footer">
|
|
1240
|
+
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
1241
|
+
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
1242
|
+
</div>
|
|
1243
|
+
</div>
|
|
1244
|
+
</div>
|
|
1245
|
+
}
|
|
1246
|
+
</div>
|
|
1247
|
+
`, styles: [".cropper-container{-webkit-touch-callout:none;direction:ltr;font-size:0;line-height:0;position:relative;touch-action:none;-webkit-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{inset:0;position:absolute}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}\n", ".fly-image-upload{display:flex;flex-direction:column;gap:6px}.fly-image-upload__hidden{display:none}.fly-image-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:12px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s;min-height:120px}.fly-image-upload__dropzone:hover{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__icon{font-size:28px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-image-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__preview{position:relative;border-radius:12px;overflow:hidden}.fly-image-upload__img{display:block;width:100%;max-height:280px;object-fit:cover;border-radius:12px}.fly-image-upload__overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:12px;background:#00000059;opacity:0;transition:opacity .15s}.fly-image-upload__preview:hover .fly-image-upload__overlay{opacity:1}.fly-image-upload__action-btn{width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;background:#ffffffe6;color:var(--label-primary, #1f2937);font-size:14px;transition:background .15s}.fly-image-upload__action-btn:hover{background:#fff}.fly-image-upload__action-btn--danger:hover{background:#fee2e2;color:#dc2626}.fly-image-upload__error{font-size:12px;color:var(--system-red, #ef4444)}.fly-image-upload__crop-backdrop{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0000004d}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden}.fly-image-upload__crop-body img{display:block;max-width:100%}.fly-image-upload__crop-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--separator-primary, #e5e7eb)}\n"] }]
|
|
1248
|
+
}], propDecorators: { aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], maxSizeBytes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeBytes", required: false }] }], currentImageId: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentImageId", required: false }] }], sourceApp: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceApp", required: false }] }], sourceEntityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityType", required: false }] }], sourceEntityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityId", required: false }] }], uploaded: [{ type: i0.Output, args: ["uploaded"] }], removed: [{ type: i0.Output, args: ["removed"] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], cropImage: [{ type: i0.ViewChild, args: ['cropImage', { isSignal: true }] }] } });
|
|
1249
|
+
|
|
1250
|
+
const FILE_ICONS = {
|
|
1251
|
+
'application/pdf': 'pi-file-pdf',
|
|
1252
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'pi-file-word',
|
|
1253
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'pi-file-excel',
|
|
1254
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pi-file',
|
|
1255
|
+
'image/': 'pi-image',
|
|
1256
|
+
'video/': 'pi-video',
|
|
1257
|
+
'audio/': 'pi-volume-up',
|
|
1258
|
+
};
|
|
1259
|
+
function iconForType(contentType) {
|
|
1260
|
+
for (const [prefix, icon] of Object.entries(FILE_ICONS)) {
|
|
1261
|
+
if (contentType.startsWith(prefix))
|
|
1262
|
+
return icon;
|
|
1263
|
+
}
|
|
1264
|
+
return 'pi-file';
|
|
1265
|
+
}
|
|
1266
|
+
function formatSize(bytes) {
|
|
1267
|
+
if (bytes < 1024)
|
|
1268
|
+
return `${bytes} B`;
|
|
1269
|
+
if (bytes < 1024 * 1024)
|
|
1270
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1271
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Multi-file upload with drag-drop, progress, and validation.
|
|
1275
|
+
*
|
|
1276
|
+
* Usage:
|
|
1277
|
+
* ```html
|
|
1278
|
+
* <fly-file-upload
|
|
1279
|
+
* [maxFiles]="5"
|
|
1280
|
+
* [maxFileSizeBytes]="5242880"
|
|
1281
|
+
* accept=".pdf,.docx,.xlsx"
|
|
1282
|
+
* [(files)]="trend.attachments"
|
|
1283
|
+
* sourceApp="circles"
|
|
1284
|
+
* sourceEntityType="trend"
|
|
1285
|
+
* (filesChanged)="onAttachmentsChanged($event)"
|
|
1286
|
+
* />
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
1289
|
+
class FlyFileUploadComponent {
|
|
1290
|
+
http = inject(HttpClient);
|
|
1291
|
+
// ── Inputs ──
|
|
1292
|
+
maxFiles = input(5, ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
|
|
1293
|
+
maxFileSizeBytes = input(5 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxFileSizeBytes" }] : /* istanbul ignore next */ []));
|
|
1294
|
+
accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
|
|
1295
|
+
sourceApp = input('unknown', ...(ngDevMode ? [{ debugName: "sourceApp" }] : /* istanbul ignore next */ []));
|
|
1296
|
+
sourceEntityType = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityType" }] : /* istanbul ignore next */ []));
|
|
1297
|
+
sourceEntityId = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityId" }] : /* istanbul ignore next */ []));
|
|
1298
|
+
/** Two-way model for completed file metadata. */
|
|
1299
|
+
files = model([], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
|
|
1300
|
+
// ── Outputs ──
|
|
1301
|
+
filesChanged = output();
|
|
1302
|
+
// ── State ──
|
|
1303
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1304
|
+
dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
|
|
1305
|
+
slots = signal([], ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
|
|
1306
|
+
/** Pre-existing files loaded from the entity (before any new uploads this session). */
|
|
1307
|
+
existingFiles = signal([], ...(ngDevMode ? [{ debugName: "existingFiles" }] : /* istanbul ignore next */ []));
|
|
1308
|
+
fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
|
|
1309
|
+
allSlots = computed(() => this.slots(), ...(ngDevMode ? [{ debugName: "allSlots" }] : /* istanbul ignore next */ []));
|
|
1310
|
+
canAddMore = computed(() => {
|
|
1311
|
+
const current = this.files().length + this.slots().filter(s => s.status === 'uploading').length;
|
|
1312
|
+
return current < this.maxFiles();
|
|
1313
|
+
}, ...(ngDevMode ? [{ debugName: "canAddMore" }] : /* istanbul ignore next */ []));
|
|
1314
|
+
limitHint = computed(() => {
|
|
1315
|
+
const mb = this.maxFileSizeBytes() / (1024 * 1024);
|
|
1316
|
+
return `Max ${this.maxFiles()} files, ${mb}MB each`;
|
|
1317
|
+
}, ...(ngDevMode ? [{ debugName: "limitHint" }] : /* istanbul ignore next */ []));
|
|
1318
|
+
triggerFileInput() {
|
|
1319
|
+
this.fileInput()?.nativeElement.click();
|
|
1320
|
+
}
|
|
1321
|
+
onDragOver(e) {
|
|
1322
|
+
e.preventDefault();
|
|
1323
|
+
e.stopPropagation();
|
|
1324
|
+
this.dragging.set(true);
|
|
1325
|
+
}
|
|
1326
|
+
onDragLeave(e) {
|
|
1327
|
+
e.preventDefault();
|
|
1328
|
+
this.dragging.set(false);
|
|
1329
|
+
}
|
|
1330
|
+
onDrop(e) {
|
|
1331
|
+
e.preventDefault();
|
|
1332
|
+
e.stopPropagation();
|
|
1333
|
+
this.dragging.set(false);
|
|
1334
|
+
const fileList = e.dataTransfer?.files;
|
|
1335
|
+
if (fileList)
|
|
1336
|
+
this.processFiles(Array.from(fileList));
|
|
1337
|
+
}
|
|
1338
|
+
onFilesSelected(e) {
|
|
1339
|
+
const input = e.target;
|
|
1340
|
+
if (input.files)
|
|
1341
|
+
this.processFiles(Array.from(input.files));
|
|
1342
|
+
input.value = '';
|
|
1343
|
+
}
|
|
1344
|
+
removeSlot(slot) {
|
|
1345
|
+
this.slots.update(s => s.filter(x => x !== slot));
|
|
1346
|
+
if (slot.info) {
|
|
1347
|
+
this.files.update(f => f.filter(x => x.id !== slot.info.id));
|
|
1348
|
+
this.filesChanged.emit(this.files());
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
removeExisting(f) {
|
|
1352
|
+
this.existingFiles.update(list => list.filter(x => x.id !== f.id));
|
|
1353
|
+
this.files.update(list => list.filter(x => x.id !== f.id));
|
|
1354
|
+
this.filesChanged.emit(this.files());
|
|
1355
|
+
}
|
|
1356
|
+
iconFor(slot) { return iconForType(slot.file.type); }
|
|
1357
|
+
iconForInfo(f) { return iconForType(f.contentType); }
|
|
1358
|
+
formatFileSize(bytes) { return formatSize(bytes); }
|
|
1359
|
+
processFiles(newFiles) {
|
|
1360
|
+
this.error.set(null);
|
|
1361
|
+
const currentCount = this.files().length + this.slots().filter(s => s.status === 'uploading').length;
|
|
1362
|
+
const remaining = this.maxFiles() - currentCount;
|
|
1363
|
+
if (remaining <= 0) {
|
|
1364
|
+
this.error.set(`Maximum ${this.maxFiles()} files allowed`);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const toUpload = newFiles.slice(0, remaining);
|
|
1368
|
+
if (newFiles.length > remaining) {
|
|
1369
|
+
this.error.set(`Only ${remaining} more file(s) can be added`);
|
|
1370
|
+
}
|
|
1371
|
+
for (const file of toUpload) {
|
|
1372
|
+
if (file.size > this.maxFileSizeBytes()) {
|
|
1373
|
+
const mb = this.maxFileSizeBytes() / (1024 * 1024);
|
|
1374
|
+
this.error.set(`"${file.name}" exceeds ${mb}MB limit`);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
this.uploadFile(file);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
uploadFile(file) {
|
|
1381
|
+
const slot = { file, progress: 0, status: 'uploading' };
|
|
1382
|
+
this.slots.update(s => [...s, slot]);
|
|
1383
|
+
const formData = new FormData();
|
|
1384
|
+
formData.append('file', file, file.name);
|
|
1385
|
+
formData.append('sourceApp', this.sourceApp());
|
|
1386
|
+
if (this.sourceEntityType())
|
|
1387
|
+
formData.append('sourceEntityType', this.sourceEntityType());
|
|
1388
|
+
if (this.sourceEntityId())
|
|
1389
|
+
formData.append('sourceEntityId', this.sourceEntityId());
|
|
1390
|
+
this.http.post('/api/files/upload', formData, {
|
|
1391
|
+
reportProgress: true,
|
|
1392
|
+
observe: 'events',
|
|
1393
|
+
}).subscribe({
|
|
1394
|
+
next: (event) => {
|
|
1395
|
+
if (event.type === HttpEventType.UploadProgress && event.total) {
|
|
1396
|
+
slot.progress = Math.round((event.loaded / event.total) * 100);
|
|
1397
|
+
this.slots.update(s => [...s]); // trigger signal change
|
|
1398
|
+
}
|
|
1399
|
+
else if (event.type === HttpEventType.Response) {
|
|
1400
|
+
const info = event.body?.data ?? event.body;
|
|
1401
|
+
slot.status = 'done';
|
|
1402
|
+
slot.progress = 100;
|
|
1403
|
+
slot.info = info;
|
|
1404
|
+
this.slots.update(s => [...s]);
|
|
1405
|
+
this.files.update(f => [...f, info]);
|
|
1406
|
+
this.filesChanged.emit(this.files());
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
error: (err) => {
|
|
1410
|
+
slot.status = 'error';
|
|
1411
|
+
slot.error = err?.error?.message ?? 'Upload failed';
|
|
1412
|
+
this.slots.update(s => [...s]);
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyFileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1417
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyFileUploadComponent, isStandalone: true, selector: "fly-file-upload", inputs: { maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxFileSizeBytes: { classPropertyName: "maxFileSizeBytes", publicName: "maxFileSizeBytes", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, sourceApp: { classPropertyName: "sourceApp", publicName: "sourceApp", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityType: { classPropertyName: "sourceEntityType", publicName: "sourceEntityType", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityId: { classPropertyName: "sourceEntityId", publicName: "sourceEntityId", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { files: "filesChange", filesChanged: "filesChanged" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1418
|
+
<div class="fly-file-upload">
|
|
1419
|
+
@if (canAddMore()) {
|
|
1420
|
+
<div class="fly-file-upload__dropzone"
|
|
1421
|
+
[class.fly-file-upload__dropzone--drag]="dragging()"
|
|
1422
|
+
(click)="triggerFileInput()"
|
|
1423
|
+
(dragover)="onDragOver($event)"
|
|
1424
|
+
(dragleave)="onDragLeave($event)"
|
|
1425
|
+
(drop)="onDrop($event)">
|
|
1426
|
+
<span class="pi pi-cloud-upload fly-file-upload__icon" aria-hidden="true"></span>
|
|
1427
|
+
<span class="fly-file-upload__label">{{ 'files.fileUpload.dropOrClick' | translate }}</span>
|
|
1428
|
+
<span class="fly-file-upload__hint">{{ limitHint() }}</span>
|
|
1429
|
+
</div>
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
@if (error()) {
|
|
1433
|
+
<div class="fly-file-upload__error">{{ error() }}</div>
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
@if (allSlots().length) {
|
|
1437
|
+
<ul class="fly-file-upload__list">
|
|
1438
|
+
@for (slot of allSlots(); track slot.file.name + slot.file.size) {
|
|
1439
|
+
<li class="fly-file-upload__item">
|
|
1440
|
+
<span class="pi {{ iconFor(slot) }} fly-file-upload__file-icon" aria-hidden="true"></span>
|
|
1441
|
+
<div class="fly-file-upload__file-info">
|
|
1442
|
+
<span class="fly-file-upload__file-name">{{ slot.file.name }}</span>
|
|
1443
|
+
<span class="fly-file-upload__file-size">{{ formatFileSize(slot.file.size) }}</span>
|
|
1444
|
+
</div>
|
|
1445
|
+
@if (slot.status === 'uploading') {
|
|
1446
|
+
<div class="fly-file-upload__progress-track">
|
|
1447
|
+
<div class="fly-file-upload__progress-fill" [style.width.%]="slot.progress"></div>
|
|
1448
|
+
</div>
|
|
1449
|
+
}
|
|
1450
|
+
@if (slot.status === 'error') {
|
|
1451
|
+
<span class="fly-file-upload__file-error" [title]="slot.error ?? ''">
|
|
1452
|
+
<span class="pi pi-exclamation-triangle" aria-hidden="true"></span>
|
|
1453
|
+
</span>
|
|
1454
|
+
}
|
|
1455
|
+
@if (slot.status === 'done') {
|
|
1456
|
+
<span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
|
|
1457
|
+
}
|
|
1458
|
+
<button type="button" class="fly-file-upload__remove-btn" (click)="removeSlot(slot)" [title]="'common.remove' | translate">
|
|
1459
|
+
<span class="pi pi-times" aria-hidden="true"></span>
|
|
1460
|
+
</button>
|
|
1461
|
+
</li>
|
|
1462
|
+
}
|
|
1463
|
+
</ul>
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
@if (existingFiles().length && !allSlots().length) {
|
|
1467
|
+
<ul class="fly-file-upload__list">
|
|
1468
|
+
@for (f of existingFiles(); track f.id) {
|
|
1469
|
+
<li class="fly-file-upload__item">
|
|
1470
|
+
<span class="pi {{ iconForInfo(f) }} fly-file-upload__file-icon" aria-hidden="true"></span>
|
|
1471
|
+
<div class="fly-file-upload__file-info">
|
|
1472
|
+
<span class="fly-file-upload__file-name">{{ f.fileName }}</span>
|
|
1473
|
+
<span class="fly-file-upload__file-size">{{ formatFileSize(f.sizeBytes) }}</span>
|
|
1474
|
+
</div>
|
|
1475
|
+
<span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
|
|
1476
|
+
<button type="button" class="fly-file-upload__remove-btn" (click)="removeExisting(f)" [title]="'common.remove' | translate">
|
|
1477
|
+
<span class="pi pi-times" aria-hidden="true"></span>
|
|
1478
|
+
</button>
|
|
1479
|
+
</li>
|
|
1480
|
+
}
|
|
1481
|
+
</ul>
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
<input #fileInput type="file" [accept]="accept()" multiple class="fly-file-upload__hidden" (change)="onFilesSelected($event)" />
|
|
1485
|
+
</div>
|
|
1486
|
+
`, isInline: true, styles: [".fly-file-upload{display:flex;flex-direction:column;gap:8px}.fly-file-upload__hidden{display:none}.fly-file-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:20px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:10px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s}.fly-file-upload__dropzone:hover,.fly-file-upload__dropzone--drag{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-file-upload__icon{font-size:24px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-file-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__error{font-size:12px;color:var(--system-red, #ef4444);padding:4px 0}.fly-file-upload__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}.fly-file-upload__item{display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--separator-primary, #e5e7eb);border-radius:8px;background:var(--fill-quaternary, #f9fafb);font-size:13px}.fly-file-upload__file-icon{font-size:18px;color:var(--label-tertiary, #9ca3af);flex-shrink:0}.fly-file-upload__file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.fly-file-upload__file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--label-primary, #1f2937)}.fly-file-upload__file-size{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__progress-track{width:60px;height:4px;border-radius:2px;background:var(--fill-tertiary, #e5e7eb);overflow:hidden}.fly-file-upload__progress-fill{height:100%;background:var(--accent-primary, #0071e3);border-radius:2px;transition:width .2s}.fly-file-upload__file-ok{color:var(--system-green, #22c55e);font-size:14px}.fly-file-upload__file-error{color:var(--system-red, #ef4444);font-size:14px}.fly-file-upload__remove-btn{background:none;border:none;cursor:pointer;color:var(--label-tertiary, #9ca3af);width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:color .15s,background .15s;flex-shrink:0}.fly-file-upload__remove-btn:hover{color:var(--system-red, #ef4444);background:var(--fill-tertiary, #f3f4f6)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1487
|
+
}
|
|
1488
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyFileUploadComponent, decorators: [{
|
|
1489
|
+
type: Component,
|
|
1490
|
+
args: [{ selector: 'fly-file-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1491
|
+
<div class="fly-file-upload">
|
|
1492
|
+
@if (canAddMore()) {
|
|
1493
|
+
<div class="fly-file-upload__dropzone"
|
|
1494
|
+
[class.fly-file-upload__dropzone--drag]="dragging()"
|
|
1495
|
+
(click)="triggerFileInput()"
|
|
1496
|
+
(dragover)="onDragOver($event)"
|
|
1497
|
+
(dragleave)="onDragLeave($event)"
|
|
1498
|
+
(drop)="onDrop($event)">
|
|
1499
|
+
<span class="pi pi-cloud-upload fly-file-upload__icon" aria-hidden="true"></span>
|
|
1500
|
+
<span class="fly-file-upload__label">{{ 'files.fileUpload.dropOrClick' | translate }}</span>
|
|
1501
|
+
<span class="fly-file-upload__hint">{{ limitHint() }}</span>
|
|
1502
|
+
</div>
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
@if (error()) {
|
|
1506
|
+
<div class="fly-file-upload__error">{{ error() }}</div>
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
@if (allSlots().length) {
|
|
1510
|
+
<ul class="fly-file-upload__list">
|
|
1511
|
+
@for (slot of allSlots(); track slot.file.name + slot.file.size) {
|
|
1512
|
+
<li class="fly-file-upload__item">
|
|
1513
|
+
<span class="pi {{ iconFor(slot) }} fly-file-upload__file-icon" aria-hidden="true"></span>
|
|
1514
|
+
<div class="fly-file-upload__file-info">
|
|
1515
|
+
<span class="fly-file-upload__file-name">{{ slot.file.name }}</span>
|
|
1516
|
+
<span class="fly-file-upload__file-size">{{ formatFileSize(slot.file.size) }}</span>
|
|
1517
|
+
</div>
|
|
1518
|
+
@if (slot.status === 'uploading') {
|
|
1519
|
+
<div class="fly-file-upload__progress-track">
|
|
1520
|
+
<div class="fly-file-upload__progress-fill" [style.width.%]="slot.progress"></div>
|
|
1521
|
+
</div>
|
|
1522
|
+
}
|
|
1523
|
+
@if (slot.status === 'error') {
|
|
1524
|
+
<span class="fly-file-upload__file-error" [title]="slot.error ?? ''">
|
|
1525
|
+
<span class="pi pi-exclamation-triangle" aria-hidden="true"></span>
|
|
1526
|
+
</span>
|
|
1527
|
+
}
|
|
1528
|
+
@if (slot.status === 'done') {
|
|
1529
|
+
<span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
|
|
1530
|
+
}
|
|
1531
|
+
<button type="button" class="fly-file-upload__remove-btn" (click)="removeSlot(slot)" [title]="'common.remove' | translate">
|
|
1532
|
+
<span class="pi pi-times" aria-hidden="true"></span>
|
|
1533
|
+
</button>
|
|
1534
|
+
</li>
|
|
1535
|
+
}
|
|
1536
|
+
</ul>
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
@if (existingFiles().length && !allSlots().length) {
|
|
1540
|
+
<ul class="fly-file-upload__list">
|
|
1541
|
+
@for (f of existingFiles(); track f.id) {
|
|
1542
|
+
<li class="fly-file-upload__item">
|
|
1543
|
+
<span class="pi {{ iconForInfo(f) }} fly-file-upload__file-icon" aria-hidden="true"></span>
|
|
1544
|
+
<div class="fly-file-upload__file-info">
|
|
1545
|
+
<span class="fly-file-upload__file-name">{{ f.fileName }}</span>
|
|
1546
|
+
<span class="fly-file-upload__file-size">{{ formatFileSize(f.sizeBytes) }}</span>
|
|
1547
|
+
</div>
|
|
1548
|
+
<span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
|
|
1549
|
+
<button type="button" class="fly-file-upload__remove-btn" (click)="removeExisting(f)" [title]="'common.remove' | translate">
|
|
1550
|
+
<span class="pi pi-times" aria-hidden="true"></span>
|
|
1551
|
+
</button>
|
|
1552
|
+
</li>
|
|
1553
|
+
}
|
|
1554
|
+
</ul>
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
<input #fileInput type="file" [accept]="accept()" multiple class="fly-file-upload__hidden" (change)="onFilesSelected($event)" />
|
|
1558
|
+
</div>
|
|
1559
|
+
`, styles: [".fly-file-upload{display:flex;flex-direction:column;gap:8px}.fly-file-upload__hidden{display:none}.fly-file-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:20px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:10px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s}.fly-file-upload__dropzone:hover,.fly-file-upload__dropzone--drag{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-file-upload__icon{font-size:24px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-file-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__error{font-size:12px;color:var(--system-red, #ef4444);padding:4px 0}.fly-file-upload__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}.fly-file-upload__item{display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--separator-primary, #e5e7eb);border-radius:8px;background:var(--fill-quaternary, #f9fafb);font-size:13px}.fly-file-upload__file-icon{font-size:18px;color:var(--label-tertiary, #9ca3af);flex-shrink:0}.fly-file-upload__file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.fly-file-upload__file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--label-primary, #1f2937)}.fly-file-upload__file-size{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__progress-track{width:60px;height:4px;border-radius:2px;background:var(--fill-tertiary, #e5e7eb);overflow:hidden}.fly-file-upload__progress-fill{height:100%;background:var(--accent-primary, #0071e3);border-radius:2px;transition:width .2s}.fly-file-upload__file-ok{color:var(--system-green, #22c55e);font-size:14px}.fly-file-upload__file-error{color:var(--system-red, #ef4444);font-size:14px}.fly-file-upload__remove-btn{background:none;border:none;cursor:pointer;color:var(--label-tertiary, #9ca3af);width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:color .15s,background .15s;flex-shrink:0}.fly-file-upload__remove-btn:hover{color:var(--system-red, #ef4444);background:var(--fill-tertiary, #f3f4f6)}\n"] }]
|
|
1560
|
+
}], propDecorators: { maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxFileSizeBytes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSizeBytes", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], sourceApp: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceApp", required: false }] }], sourceEntityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityType", required: false }] }], sourceEntityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityId", required: false }] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], filesChanged: [{ type: i0.Output, args: ["filesChanged"] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
|
|
1561
|
+
|
|
683
1562
|
/*
|
|
684
1563
|
* @mohamedatia/fly-design-system — Public API
|
|
685
1564
|
* https://www.npmjs.com/package/@mohamedatia/fly-design-system
|
|
686
1565
|
*
|
|
687
|
-
* This is the single entry point for Business App developers.
|
|
1566
|
+
* This is the single entry point for Business / Supporting App developers.
|
|
688
1567
|
* Import everything from '@mohamedatia/fly-design-system' — never from relative shell paths.
|
|
689
1568
|
*
|
|
690
1569
|
* NOTE: This package is published under @mohamedatia/fly-design-system until the @fly
|
|
@@ -703,12 +1582,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
703
1582
|
* shell to design system. MessageBoxService + MessageBoxComponent added (Windows Forms-style
|
|
704
1583
|
* modal dialogs with localized button labels, focus management, and Escape key support).
|
|
705
1584
|
* MessageBoxButtons, MessageBoxIcon, DialogResult, MessageBoxOptions, MessageBoxButton exported.
|
|
706
|
-
*
|
|
1585
|
+
* v1.5.0: SharePanelComponent — generic share/ACL overlay; host supplies API callbacks and optional
|
|
1586
|
+
* permission level options (file ACL vs note-style View/Edit).
|
|
1587
|
+
* v1.6.0: SharePanelComponent — `loadOuTree(chartId: string | null)`; optional `loadChartOptions` for
|
|
1588
|
+
* default vs alternative org charts (OU grants still use real OU ids). `ShareOrgChartOption` exported.
|
|
1589
|
+
* v1.7.0: SharePanelComponent — `loadChartOptions` and `loadOuLabelMap` are required; chart tree loads after
|
|
1590
|
+
* options resolve (no spurious null fetch). `loadOuLabelMap` supplies default-tree labels for OU rows.
|
|
1591
|
+
* v1.8.0: `WindowInstance` optional `contentUiBlocked` / `contentUiBlockMessageKey`; `WindowManagerService`
|
|
1592
|
+
* `beginContentUiBlock` / `endContentUiBlock` default no-ops; `FlyBlockUiComponent` for shell window overlay.
|
|
1593
|
+
* See docs/ExternalAppsGuide/03-frontend-app.md.
|
|
707
1594
|
*/
|
|
708
1595
|
|
|
709
1596
|
/**
|
|
710
1597
|
* Generated bundle index. Do not edit.
|
|
711
1598
|
*/
|
|
712
1599
|
|
|
713
|
-
export { AuthService, ContextMenuComponent, DialogResult, FlyThemeService, I18nService, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, isRtlLocale };
|
|
1600
|
+
export { AuthService, ContextMenuComponent, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_THEME_MODE_IDS, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyThemeService, I18nService, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, isRtlLocale, normalizeFlyTheme };
|
|
714
1601
|
//# sourceMappingURL=mohamedatia-fly-design-system.mjs.map
|