@mohamedatia/fly-design-system 1.5.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 +705 -16
- 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 +201 -8
- 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,11 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, Output, Input } from '@angular/core';
|
|
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 { CommonModule } from '@angular/common';
|
|
5
|
+
import { of, ReplaySubject } from 'rxjs';
|
|
6
6
|
import * as i1 from '@angular/forms';
|
|
7
7
|
import { FormsModule } from '@angular/forms';
|
|
8
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';
|
|
9
12
|
|
|
10
13
|
const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
|
|
11
14
|
|
|
@@ -229,21 +232,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
229
232
|
args: [{ providedIn: 'root' }]
|
|
230
233
|
}] });
|
|
231
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
|
+
}
|
|
232
246
|
/**
|
|
233
|
-
* Applies `html.light-theme` / `html.
|
|
247
|
+
* Applies `html.light-theme` / `html.spatial-theme` / `html.dark-theme` for DS SCSS.
|
|
234
248
|
* Shell and standalone Business Apps use the same service (federation singleton when shared).
|
|
235
249
|
*/
|
|
236
250
|
class FlyThemeService {
|
|
237
|
-
|
|
251
|
+
platformId = inject(PLATFORM_ID);
|
|
252
|
+
theme = signal(DEFAULT_FLY_THEME_MODE, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
238
253
|
applyTheme(mode) {
|
|
239
254
|
this.theme.set(mode);
|
|
255
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
240
258
|
const html = document.documentElement;
|
|
241
|
-
html.classList.remove('
|
|
242
|
-
if (mode === '
|
|
243
|
-
html.classList.add('
|
|
259
|
+
html.classList.remove('light-theme', 'spatial-theme', 'dark-theme');
|
|
260
|
+
if (mode === 'spatial') {
|
|
261
|
+
html.classList.add('spatial-theme');
|
|
244
262
|
}
|
|
245
|
-
else if (mode === '
|
|
246
|
-
html.classList.add('
|
|
263
|
+
else if (mode === 'dark') {
|
|
264
|
+
html.classList.add('dark-theme');
|
|
247
265
|
}
|
|
248
266
|
else {
|
|
249
267
|
html.classList.add('light-theme');
|
|
@@ -264,6 +282,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
264
282
|
* on the shell's internal implementation.
|
|
265
283
|
*/
|
|
266
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) { }
|
|
267
292
|
}
|
|
268
293
|
/**
|
|
269
294
|
* No-op fallback implementation used when running a Business App in
|
|
@@ -352,6 +377,13 @@ class MockAuthService {
|
|
|
352
377
|
hasPendingTwoFactor;
|
|
353
378
|
/** Always false in mock mode — auth is pre-populated synchronously. */
|
|
354
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
|
+
}
|
|
355
387
|
constructor() {
|
|
356
388
|
// All signal/computed calls are inside the constructor body so Angular's
|
|
357
389
|
// injection context and ngDevMode are fully set up before they execute.
|
|
@@ -718,7 +750,18 @@ class SharePanelComponent {
|
|
|
718
750
|
/** Load current shares / permissions (host resolves display names when possible). */
|
|
719
751
|
loadPermissions;
|
|
720
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
|
+
*/
|
|
721
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;
|
|
722
765
|
grantToUser;
|
|
723
766
|
grantToOu;
|
|
724
767
|
updatePermission;
|
|
@@ -740,17 +783,49 @@ class SharePanelComponent {
|
|
|
740
783
|
searchResults = signal([], ...(ngDevMode ? [{ debugName: "searchResults" }] : /* istanbul ignore next */ []));
|
|
741
784
|
ouTree = signal([], ...(ngDevMode ? [{ debugName: "ouTree" }] : /* istanbul ignore next */ []));
|
|
742
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 */ []));
|
|
743
790
|
searchTimeout = null;
|
|
791
|
+
/** Emits after chart options load; further emissions on user chart changes (no initial null). */
|
|
792
|
+
selectedChartId$ = new ReplaySubject(1);
|
|
744
793
|
ngOnInit() {
|
|
745
794
|
const levels = this.levelOptions;
|
|
746
795
|
this.newLevel = levels[0]?.value ?? 'View';
|
|
747
796
|
this.refreshPermissions();
|
|
748
|
-
this.
|
|
797
|
+
this.loadOuLabelMap()
|
|
749
798
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
750
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({
|
|
751
806
|
next: (tree) => this.ouTree.set(tree),
|
|
752
807
|
error: () => this.ouTree.set([]),
|
|
753
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);
|
|
754
829
|
}
|
|
755
830
|
get levelOptions() {
|
|
756
831
|
const p = this.permissionLevels;
|
|
@@ -819,6 +894,11 @@ class SharePanelComponent {
|
|
|
819
894
|
return 'pi pi-globe';
|
|
820
895
|
return 'pi pi-question';
|
|
821
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
|
+
}
|
|
822
902
|
getPermLabel(perm) {
|
|
823
903
|
if (perm.displayName?.trim())
|
|
824
904
|
return perm.displayName.trim();
|
|
@@ -826,21 +906,25 @@ class SharePanelComponent {
|
|
|
826
906
|
return `${perm.grantedToUserId.substring(0, 8)}…`;
|
|
827
907
|
}
|
|
828
908
|
if (perm.grantedToOuId) {
|
|
829
|
-
const
|
|
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);
|
|
830
914
|
if (ou)
|
|
831
915
|
return ou.displayName;
|
|
832
|
-
return `${
|
|
916
|
+
return `${id.substring(0, 8)}…`;
|
|
833
917
|
}
|
|
834
918
|
if (perm.grantedToAppId === '*')
|
|
835
919
|
return this.i18n.t(this.everyoneLabelKey);
|
|
836
920
|
return perm.grantedToAppId ?? '';
|
|
837
921
|
}
|
|
838
922
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
839
|
-
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", 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 (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 class=\"fac-ou-item\" (click)=\"onGrantToOu(ou)\">\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:480px;max-height:80vh;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-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:120px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 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 });
|
|
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 });
|
|
840
924
|
}
|
|
841
925
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, decorators: [{
|
|
842
926
|
type: Component,
|
|
843
|
-
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 (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
|
|
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"] }]
|
|
844
928
|
}], propDecorators: { targetName: [{
|
|
845
929
|
type: Input
|
|
846
930
|
}], titleKey: [{
|
|
@@ -854,6 +938,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
854
938
|
}], loadOuTree: [{
|
|
855
939
|
type: Input,
|
|
856
940
|
args: [{ required: true }]
|
|
941
|
+
}], loadChartOptions: [{
|
|
942
|
+
type: Input,
|
|
943
|
+
args: [{ required: true }]
|
|
944
|
+
}], loadOuLabelMap: [{
|
|
945
|
+
type: Input,
|
|
946
|
+
args: [{ required: true }]
|
|
857
947
|
}], grantToUser: [{
|
|
858
948
|
type: Input,
|
|
859
949
|
args: [{ required: true }]
|
|
@@ -876,6 +966,599 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
876
966
|
type: Output
|
|
877
967
|
}] } });
|
|
878
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
|
+
|
|
879
1562
|
/*
|
|
880
1563
|
* @mohamedatia/fly-design-system — Public API
|
|
881
1564
|
* https://www.npmjs.com/package/@mohamedatia/fly-design-system
|
|
@@ -901,6 +1584,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
901
1584
|
* MessageBoxButtons, MessageBoxIcon, DialogResult, MessageBoxOptions, MessageBoxButton exported.
|
|
902
1585
|
* v1.5.0: SharePanelComponent — generic share/ACL overlay; host supplies API callbacks and optional
|
|
903
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.
|
|
904
1593
|
* See docs/ExternalAppsGuide/03-frontend-app.md.
|
|
905
1594
|
*/
|
|
906
1595
|
|
|
@@ -908,5 +1597,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
908
1597
|
* Generated bundle index. Do not edit.
|
|
909
1598
|
*/
|
|
910
1599
|
|
|
911
|
-
export { AuthService, ContextMenuComponent, DialogResult, FlyThemeService, I18nService, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, 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 };
|
|
912
1601
|
//# sourceMappingURL=mohamedatia-fly-design-system.mjs.map
|