@mohamedatia/fly-design-system 2.13.0 → 2.14.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 +1148 -278
- package/fesm2022/mohamedatia-fly-design-system.mjs.map +1 -1
- package/package.json +10 -1
- package/scss/_business-app-buttons.scss +213 -213
- package/scss/_fly-theme.scss +8 -8
- package/scss/_theme-auto.scss +6 -6
- package/scss/_theme-dark-vars.scss +49 -49
- package/scss/_theme-dark.scss +142 -142
- package/scss/_theme-light.scss +108 -108
- package/scss/_tokens.scss +115 -115
- package/types/mohamedatia-fly-design-system.d.ts +1116 -814
- package/types/mohamedatia-fly-design-system.d.ts.map +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, DestroyRef, ChangeDetectionStrategy, Component, Pipe,
|
|
2
|
+
import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, DestroyRef, ChangeDetectionStrategy, Component, Pipe, ElementRef, input, output, afterNextRender, HostListener, ViewChild, ChangeDetectorRef, forwardRef, DOCUMENT, EventEmitter, Output, Input, Injector, viewChild, effect, ViewEncapsulation, model, Directive, Renderer2 } from '@angular/core';
|
|
3
3
|
import * as i1$1 from '@angular/common';
|
|
4
4
|
import { isPlatformBrowser, NgComponentOutlet, CommonModule, DOCUMENT as DOCUMENT$1 } from '@angular/common';
|
|
5
5
|
import { Router, NavigationEnd } from '@angular/router';
|
|
@@ -7,8 +7,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
|
7
7
|
import { of, ReplaySubject, Subject } from 'rxjs';
|
|
8
8
|
import * as i1 from '@angular/forms';
|
|
9
9
|
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
10
|
+
import { HttpClient, HttpParams, HttpEventType } from '@angular/common/http';
|
|
11
|
+
import { Editor } from '@tiptap/core';
|
|
12
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
13
|
+
import { Markdown } from '@tiptap/markdown';
|
|
14
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
15
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
16
|
+
import { TiptapEditorDirective } from 'ngx-tiptap';
|
|
10
17
|
import { switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
|
|
11
|
-
import { HttpClient, HttpEventType } from '@angular/common/http';
|
|
12
18
|
import Cropper from 'cropperjs';
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -262,6 +268,144 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
262
268
|
args: [{ providedIn: 'root' }]
|
|
263
269
|
}] });
|
|
264
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Baseline UI strings for design-system components (markdown editor, entity
|
|
273
|
+
* lookup, …) in the four platform locales (`en`, `ar`, `fr`, `ur`).
|
|
274
|
+
*
|
|
275
|
+
* These are registered as the **lowest-priority** layer of {@link I18nService}
|
|
276
|
+
* (below the shell layer and any remote bundle), so DS components render real,
|
|
277
|
+
* localized labels even in a **standalone** consumer — a Business App running
|
|
278
|
+
* outside the desktop shell that never called `setShellTranslations()` — which
|
|
279
|
+
* would otherwise see raw keys (`common.label.bold`) as the pipe's fallback.
|
|
280
|
+
*
|
|
281
|
+
* Precedence: any consumer-supplied key with the same name still wins, so this
|
|
282
|
+
* is a safe default that an integrator can override without coordination.
|
|
283
|
+
*
|
|
284
|
+
* Scope: only the keys the **shippable** DS components reference today
|
|
285
|
+
* (`common.*` toolbar/link labels + `agent.lookup.*`). When a new DS component
|
|
286
|
+
* starts using `| translate`, add its keys here so it stays self-sufficient.
|
|
287
|
+
* Values are copied verbatim from the shell's `public/locale/*.json` — keep
|
|
288
|
+
* them in sync if either side changes.
|
|
289
|
+
*/
|
|
290
|
+
const DS_BASELINE_LOCALES = {
|
|
291
|
+
en: {
|
|
292
|
+
'common.action.cancel': 'Cancel',
|
|
293
|
+
'common.label.apply': 'Apply',
|
|
294
|
+
'common.label.formatting_toolbar': 'Formatting toolbar',
|
|
295
|
+
'common.label.https': 'https://…',
|
|
296
|
+
'common.label.insert_entity_link': 'Insert in-app link',
|
|
297
|
+
'common.label.link': 'Link',
|
|
298
|
+
'common.label.blockquote': 'Quote',
|
|
299
|
+
'common.label.bold': 'Bold',
|
|
300
|
+
'common.label.bullet_list': 'Bulleted list',
|
|
301
|
+
'common.label.code_block': 'Code block',
|
|
302
|
+
'common.label.heading1': 'Heading 1',
|
|
303
|
+
'common.label.heading2': 'Heading 2',
|
|
304
|
+
'common.label.heading3': 'Heading 3',
|
|
305
|
+
'common.label.horizontal_rule': 'Divider',
|
|
306
|
+
'common.label.inline_code': 'Inline code',
|
|
307
|
+
'common.label.italic': 'Italic',
|
|
308
|
+
'common.label.numbered_list': 'Numbered list',
|
|
309
|
+
'common.label.redo': 'Redo',
|
|
310
|
+
'common.label.strikethrough': 'Strikethrough',
|
|
311
|
+
'common.label.task_list': 'Task list',
|
|
312
|
+
'common.label.underline': 'Underline',
|
|
313
|
+
'common.label.undo': 'Undo',
|
|
314
|
+
'agent.lookup.back_aria': 'Back to entity types',
|
|
315
|
+
'agent.lookup.dialog_aria': 'Entity lookup',
|
|
316
|
+
'agent.lookup.no_entities': 'No items available to reference.',
|
|
317
|
+
'agent.lookup.no_results': 'No matches found.',
|
|
318
|
+
'agent.lookup.searching': 'Searching…',
|
|
319
|
+
},
|
|
320
|
+
ar: {
|
|
321
|
+
'common.action.cancel': 'إلغاء',
|
|
322
|
+
'common.label.apply': 'تطبيق',
|
|
323
|
+
'common.label.formatting_toolbar': 'شريط أدوات التنسيق',
|
|
324
|
+
'common.label.https': 'https://…',
|
|
325
|
+
'common.label.insert_entity_link': 'إدراج رابط داخل التطبيق',
|
|
326
|
+
'common.label.link': 'رابط',
|
|
327
|
+
'common.label.blockquote': 'اقتباس',
|
|
328
|
+
'common.label.bold': 'غامق',
|
|
329
|
+
'common.label.bullet_list': 'قائمة نقطية',
|
|
330
|
+
'common.label.code_block': 'كتلة شيفرة',
|
|
331
|
+
'common.label.heading1': 'عنوان 1',
|
|
332
|
+
'common.label.heading2': 'عنوان 2',
|
|
333
|
+
'common.label.heading3': 'عنوان 3',
|
|
334
|
+
'common.label.horizontal_rule': 'فاصل',
|
|
335
|
+
'common.label.inline_code': 'كود مضمّن',
|
|
336
|
+
'common.label.italic': 'مائل',
|
|
337
|
+
'common.label.numbered_list': 'قائمة مرقَّمة',
|
|
338
|
+
'common.label.redo': 'إعادة',
|
|
339
|
+
'common.label.strikethrough': 'يتوسطه خط',
|
|
340
|
+
'common.label.task_list': 'قائمة مهام',
|
|
341
|
+
'common.label.underline': 'تسطير',
|
|
342
|
+
'common.label.undo': 'تراجع',
|
|
343
|
+
'agent.lookup.back_aria': 'العودة إلى أنواع العناصر',
|
|
344
|
+
'agent.lookup.dialog_aria': 'البحث عن عنصر',
|
|
345
|
+
'agent.lookup.no_entities': 'لا توجد عناصر متاحة للإشارة إليها.',
|
|
346
|
+
'agent.lookup.no_results': 'لا توجد نتائج مطابقة.',
|
|
347
|
+
'agent.lookup.searching': 'جارٍ البحث…',
|
|
348
|
+
},
|
|
349
|
+
fr: {
|
|
350
|
+
'common.action.cancel': 'Annuler',
|
|
351
|
+
'common.label.apply': 'Appliquer',
|
|
352
|
+
'common.label.formatting_toolbar': "Barre d'outils de mise en forme",
|
|
353
|
+
'common.label.https': 'https://…',
|
|
354
|
+
'common.label.insert_entity_link': 'Insérer un lien interne',
|
|
355
|
+
'common.label.link': 'Lien',
|
|
356
|
+
'common.label.blockquote': 'Citation',
|
|
357
|
+
'common.label.bold': 'Gras',
|
|
358
|
+
'common.label.bullet_list': 'Liste à puces',
|
|
359
|
+
'common.label.code_block': 'Bloc de code',
|
|
360
|
+
'common.label.heading1': 'Titre 1',
|
|
361
|
+
'common.label.heading2': 'Titre 2',
|
|
362
|
+
'common.label.heading3': 'Titre 3',
|
|
363
|
+
'common.label.horizontal_rule': 'Séparateur',
|
|
364
|
+
'common.label.inline_code': 'Code en ligne',
|
|
365
|
+
'common.label.italic': 'Italique',
|
|
366
|
+
'common.label.numbered_list': 'Liste numérotée',
|
|
367
|
+
'common.label.redo': 'Rétablir',
|
|
368
|
+
'common.label.strikethrough': 'Barré',
|
|
369
|
+
'common.label.task_list': 'Liste de tâches',
|
|
370
|
+
'common.label.underline': 'Souligner',
|
|
371
|
+
'common.label.undo': 'Annuler',
|
|
372
|
+
'agent.lookup.back_aria': "Retour aux types d'entité",
|
|
373
|
+
'agent.lookup.dialog_aria': "Recherche d'entité",
|
|
374
|
+
'agent.lookup.no_entities': 'Aucun élément disponible à référencer.',
|
|
375
|
+
'agent.lookup.no_results': 'Aucun résultat trouvé.',
|
|
376
|
+
'agent.lookup.searching': 'Recherche en cours…',
|
|
377
|
+
},
|
|
378
|
+
ur: {
|
|
379
|
+
'common.action.cancel': 'منسوخ کریں',
|
|
380
|
+
'common.label.apply': 'لاگو کریں',
|
|
381
|
+
'common.label.formatting_toolbar': 'فارمیٹنگ ٹول بار',
|
|
382
|
+
'common.label.https': 'https://…',
|
|
383
|
+
'common.label.insert_entity_link': 'ان-ایپ لنک داخل کریں',
|
|
384
|
+
'common.label.link': 'لنک',
|
|
385
|
+
'common.label.blockquote': 'اقتباس',
|
|
386
|
+
'common.label.bold': 'موٹا',
|
|
387
|
+
'common.label.bullet_list': 'بلٹ فہرست',
|
|
388
|
+
'common.label.code_block': 'کوڈ بلاک',
|
|
389
|
+
'common.label.heading1': 'سرخی 1',
|
|
390
|
+
'common.label.heading2': 'سرخی 2',
|
|
391
|
+
'common.label.heading3': 'سرخی 3',
|
|
392
|
+
'common.label.horizontal_rule': 'تقسیم لائن',
|
|
393
|
+
'common.label.inline_code': 'ان لائن کوڈ',
|
|
394
|
+
'common.label.italic': 'ترچھا',
|
|
395
|
+
'common.label.numbered_list': 'نمبر والی فہرست',
|
|
396
|
+
'common.label.redo': 'دوبارہ کریں',
|
|
397
|
+
'common.label.strikethrough': 'خط زدہ',
|
|
398
|
+
'common.label.task_list': 'ٹاسک فہرست',
|
|
399
|
+
'common.label.underline': 'خط کشید',
|
|
400
|
+
'common.label.undo': 'واپس کریں',
|
|
401
|
+
'agent.lookup.back_aria': 'عناصر کی اقسام پر واپس جائیں',
|
|
402
|
+
'agent.lookup.dialog_aria': 'عنصر کی تلاش',
|
|
403
|
+
'agent.lookup.no_entities': 'حوالہ دینے کے لیے کوئی آئٹم دستیاب نہیں۔',
|
|
404
|
+
'agent.lookup.no_results': 'کوئی مماثل نتیجہ نہیں ملا۔',
|
|
405
|
+
'agent.lookup.searching': 'تلاش جاری ہے…',
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
265
409
|
/** Locales that use RTL layout for `dir` and DS `isRtl` / `direction`. */
|
|
266
410
|
const RTL_LOCALE_SET = new Set(['ar', 'ur']);
|
|
267
411
|
function isRtlLocale(lang) {
|
|
@@ -296,8 +440,12 @@ function normalizeLocaleJson(raw) {
|
|
|
296
440
|
/**
|
|
297
441
|
* Shared I18nService for the shell and Business Apps.
|
|
298
442
|
*
|
|
299
|
-
* **Merge order** (later keys win): shell layer → remote bundles
|
|
443
|
+
* **Merge order** (later keys win): DS baseline → shell layer → remote bundles
|
|
444
|
+
* in registration order.
|
|
300
445
|
*
|
|
446
|
+
* - Baseline: {@link DS_BASELINE_LOCALES} — built-in strings for DS components
|
|
447
|
+
* (markdown editor, entity lookup) so they render localized labels even in a
|
|
448
|
+
* standalone consumer that never populated the shell layer. Always overridable.
|
|
301
449
|
* - Shell: `setShellTranslations()` after loading `locale/{lang}.json` and API overrides.
|
|
302
450
|
* - Remotes: `loadBundle()` per manifest `localeBaseUrl`.
|
|
303
451
|
*/
|
|
@@ -308,11 +456,14 @@ class I18nService {
|
|
|
308
456
|
_bundleOrder = signal([], ...(ngDevMode ? [{ debugName: "_bundleOrder" }] : /* istanbul ignore next */ []));
|
|
309
457
|
_locale = signal('en', ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
|
|
310
458
|
_version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
|
|
459
|
+
/** Built-in DS strings for the active locale (falls back to `en` for any
|
|
460
|
+
* locale we don't ship). Lowest-priority layer — always overridable. */
|
|
461
|
+
_baseline = computed(() => DS_BASELINE_LOCALES[this._locale()] ?? DS_BASELINE_LOCALES['en'] ?? {}, ...(ngDevMode ? [{ debugName: "_baseline" }] : /* istanbul ignore next */ []));
|
|
311
462
|
_merged = computed(() => {
|
|
312
463
|
const shell = this._shell();
|
|
313
464
|
const order = this._bundleOrder();
|
|
314
465
|
const bundles = this._bundles();
|
|
315
|
-
let out = { ...shell };
|
|
466
|
+
let out = { ...this._baseline(), ...shell };
|
|
316
467
|
for (const id of order) {
|
|
317
468
|
const b = bundles[id];
|
|
318
469
|
if (b)
|
|
@@ -1628,6 +1779,891 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
1628
1779
|
type: Injectable
|
|
1629
1780
|
}], ctorParameters: () => [] });
|
|
1630
1781
|
|
|
1782
|
+
/**
|
|
1783
|
+
* Singleton registry of entity lookups offered by the `/lookup` typeahead.
|
|
1784
|
+
*
|
|
1785
|
+
* Mirrors {@link AgentCommandRegistry}'s federation-singleton story
|
|
1786
|
+
* (`sharedMappings: ['@mohamedatia/fly-design-system']`), id-collision
|
|
1787
|
+
* "latest wins" contract, and disposable-handle ergonomics. OS-core entities
|
|
1788
|
+
* (note / calendar event / file) register once at shell bootstrap via
|
|
1789
|
+
* `CORE_APP_LOOKUPS`; federated remotes (Circles: scenario / trend / signal)
|
|
1790
|
+
* register at remote-component boot and dispose on window close.
|
|
1791
|
+
*
|
|
1792
|
+
* **Scope semantics diverge from commands.** Commands HIDE when their `appId`
|
|
1793
|
+
* isn't in `liveAppIds`. Lookups DO NOT — they're always offered, and
|
|
1794
|
+
* `{appId}` is just a *priority hint* that bumps that lookup to the top of
|
|
1795
|
+
* the entity picker when the app is live. See {@link LookupRegistration.scope}
|
|
1796
|
+
* for the rationale.
|
|
1797
|
+
*
|
|
1798
|
+
* Storage is a signal store keyed on {@link LookupRegistration.entity}. Because
|
|
1799
|
+
* `entity` is the collision key, an app re-registering the same entity replaces
|
|
1800
|
+
* the prior descriptor; a stale handle's `dispose()` then no-ops.
|
|
1801
|
+
*/
|
|
1802
|
+
class AgentLookupRegistry {
|
|
1803
|
+
_lookups = signal([], ...(ngDevMode ? [{ debugName: "_lookups" }] : /* istanbul ignore next */ []));
|
|
1804
|
+
/** All currently-registered lookups, in insertion order. */
|
|
1805
|
+
all = this._lookups.asReadonly();
|
|
1806
|
+
/**
|
|
1807
|
+
* All registered lookups, sorted by affinity to `liveAppIds`:
|
|
1808
|
+
*
|
|
1809
|
+
* 1. Lookups whose `scope.appId` is in the live app set (in registration
|
|
1810
|
+
* order within that bucket).
|
|
1811
|
+
* 2. Then everything else — `'global'` lookups AND scoped lookups whose
|
|
1812
|
+
* app isn't currently live — in registration order.
|
|
1813
|
+
*
|
|
1814
|
+
* Recomputes when either the registry or `liveAppIds` changes. Pass a
|
|
1815
|
+
* `Signal<ReadonlySet<string>>` from the host's app-registry for reactive
|
|
1816
|
+
* re-sorting. **Always returns the full registry** — see the type doc on
|
|
1817
|
+
* {@link LookupRegistration.scope} for why this differs from
|
|
1818
|
+
* {@link AgentCommandRegistry.visible}.
|
|
1819
|
+
*/
|
|
1820
|
+
visible(liveAppIds) {
|
|
1821
|
+
const liveSignal = isSignal$1(liveAppIds)
|
|
1822
|
+
? liveAppIds
|
|
1823
|
+
: signal(liveAppIds).asReadonly();
|
|
1824
|
+
return computed(() => {
|
|
1825
|
+
const live = liveSignal();
|
|
1826
|
+
const all = this._lookups();
|
|
1827
|
+
const prioritized = [];
|
|
1828
|
+
const rest = [];
|
|
1829
|
+
for (const l of all) {
|
|
1830
|
+
if (l.scope !== 'global' && live.has(l.scope.appId)) {
|
|
1831
|
+
prioritized.push(l);
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
rest.push(l);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return prioritized.length === 0 ? all : [...prioritized, ...rest];
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Register one lookup. Returns a handle whose `dispose()` removes the row by
|
|
1842
|
+
* `entity`. A later re-registration of the same entity makes the original
|
|
1843
|
+
* handle's `dispose()` a no-op (the newer registration owns the row).
|
|
1844
|
+
*/
|
|
1845
|
+
register(lookup) {
|
|
1846
|
+
const generation = ++this._generation;
|
|
1847
|
+
this._lookups.update((rows) => [
|
|
1848
|
+
...rows.filter((r) => r.entity !== lookup.entity),
|
|
1849
|
+
lookup,
|
|
1850
|
+
]);
|
|
1851
|
+
this._owners.set(lookup.entity, generation);
|
|
1852
|
+
return {
|
|
1853
|
+
dispose: () => {
|
|
1854
|
+
if (this._owners.get(lookup.entity) === generation) {
|
|
1855
|
+
this._owners.delete(lookup.entity);
|
|
1856
|
+
this._lookups.update((rows) => rows.filter((r) => r.entity !== lookup.entity));
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Bulk register. Rolls back on a duplicate entity WITHIN the input batch
|
|
1863
|
+
* (throws before any row lands). Cross-batch duplicates against existing rows
|
|
1864
|
+
* follow the standard "latest wins" rule and do NOT trigger rollback.
|
|
1865
|
+
*/
|
|
1866
|
+
registerAll(lookups) {
|
|
1867
|
+
const seen = new Set();
|
|
1868
|
+
for (const l of lookups) {
|
|
1869
|
+
if (seen.has(l.entity)) {
|
|
1870
|
+
throw new Error(`AgentLookupRegistry.registerAll: duplicate entity "${l.entity}" in batch`);
|
|
1871
|
+
}
|
|
1872
|
+
seen.add(l.entity);
|
|
1873
|
+
}
|
|
1874
|
+
const handles = lookups.map((l) => this.register(l));
|
|
1875
|
+
let disposed = false;
|
|
1876
|
+
return {
|
|
1877
|
+
dispose: () => {
|
|
1878
|
+
if (disposed)
|
|
1879
|
+
return;
|
|
1880
|
+
disposed = true;
|
|
1881
|
+
for (const h of handles)
|
|
1882
|
+
h.dispose();
|
|
1883
|
+
},
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Resolve a deep-link anchor to a concrete launch target.
|
|
1888
|
+
*
|
|
1889
|
+
* `kind` is the dotted `<appId>.<entity>` token the agents backend emits
|
|
1890
|
+
* inside `flyos:<kind>/<id>` chat-answer anchors — the same entity-kind
|
|
1891
|
+
* vocabulary as drag-payload kinds and `ref` parts. Returns
|
|
1892
|
+
* `{ appId, route }` when a registered lookup for that `(appId, entity)`
|
|
1893
|
+
* pair carries a {@link LookupDescriptor.deepLinkRoute} template; `null`
|
|
1894
|
+
* otherwise (unknown entity, app mismatch, or no template — e.g. the
|
|
1895
|
+
* owning app isn't installed) so the caller renders plain text rather than
|
|
1896
|
+
* a dead link.
|
|
1897
|
+
*
|
|
1898
|
+
* `appId` and `entity` are both dot-free by their own grammars, so the
|
|
1899
|
+
* FIRST dot is the unambiguous split point; a dotless `kind` can't carry an
|
|
1900
|
+
* app and never resolves. The template's single `{id}` placeholder is
|
|
1901
|
+
* substituted URL-encoded.
|
|
1902
|
+
*/
|
|
1903
|
+
resolveDeepLink(kind, id) {
|
|
1904
|
+
if (!kind)
|
|
1905
|
+
return null;
|
|
1906
|
+
const dot = kind.indexOf('.');
|
|
1907
|
+
if (dot <= 0 || dot >= kind.length - 1)
|
|
1908
|
+
return null;
|
|
1909
|
+
const appId = kind.slice(0, dot);
|
|
1910
|
+
const entity = kind.slice(dot + 1);
|
|
1911
|
+
const target = this.resolveDeepLinkForEntity(entity, id);
|
|
1912
|
+
// App must match the one named in the anchor — a `flyos:` href carries
|
|
1913
|
+
// the owning app explicitly, so a stale/wrong app prefix must NOT resolve
|
|
1914
|
+
// even when the bare entity name happens to be registered elsewhere.
|
|
1915
|
+
return target && target.appId === appId ? target : null;
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Resolve a deep-link target from a bare `(entity, id)` pair — the shape a
|
|
1919
|
+
* `/lookup` ref carries (it has no `<appId>.<entity>` kind token; the owning
|
|
1920
|
+
* app is implicit in the registered descriptor). `entity` is the registry's
|
|
1921
|
+
* unique storage key, so it identifies the descriptor unambiguously without
|
|
1922
|
+
* an app prefix.
|
|
1923
|
+
*
|
|
1924
|
+
* Returns `{ appId, route }` (the descriptor's {@link LookupDescriptor.appId}
|
|
1925
|
+
* / affinity `scope.appId` as the owner, `{id}` substituted URL-encoded) when
|
|
1926
|
+
* a matching descriptor carries a {@link LookupDescriptor.deepLinkRoute};
|
|
1927
|
+
* `null` otherwise (unknown entity, no template, or the owning app has since
|
|
1928
|
+
* unregistered) so callers render plain text rather than a dead link — the
|
|
1929
|
+
* same graceful-degrade contract as {@link resolveDeepLink}.
|
|
1930
|
+
*/
|
|
1931
|
+
resolveDeepLinkForEntity(entity, id) {
|
|
1932
|
+
if (!entity || !id)
|
|
1933
|
+
return null;
|
|
1934
|
+
const reg = this._lookups().find((r) => r.entity === entity && r.deepLinkRoute);
|
|
1935
|
+
const appId = reg ? ownerAppId(reg) : undefined;
|
|
1936
|
+
if (!reg?.deepLinkRoute || !appId)
|
|
1937
|
+
return null;
|
|
1938
|
+
return { appId, route: reg.deepLinkRoute.replace('{id}', encodeURIComponent(id)) };
|
|
1939
|
+
}
|
|
1940
|
+
/** Tear down by entity. Idempotent. */
|
|
1941
|
+
unregister(entity) {
|
|
1942
|
+
if (this._owners.delete(entity)) {
|
|
1943
|
+
this._lookups.update((rows) => rows.filter((r) => r.entity !== entity));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
/** Monotonic counter; identifies which registration call currently owns each entity. */
|
|
1947
|
+
_generation = 0;
|
|
1948
|
+
/** entity → generation. Lets a stale handle's `dispose()` no-op after replacement. */
|
|
1949
|
+
_owners = new Map();
|
|
1950
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1951
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, providedIn: 'root' });
|
|
1952
|
+
}
|
|
1953
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, decorators: [{
|
|
1954
|
+
type: Injectable,
|
|
1955
|
+
args: [{ providedIn: 'root' }]
|
|
1956
|
+
}] });
|
|
1957
|
+
/** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
|
|
1958
|
+
function isSignal$1(v) {
|
|
1959
|
+
return typeof v === 'function';
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* The app that owns a registration, for deep-link resolution. Prefers the
|
|
1963
|
+
* stamped {@link LookupDescriptor.appId} (set by `RemoteManifestService` /
|
|
1964
|
+
* `CORE_APP_LOOKUPS`), falling back to the affinity `scope.appId` so a raw
|
|
1965
|
+
* manifest descriptor still resolves even before the host stamps it.
|
|
1966
|
+
* Returns `undefined` for `'global'`-scoped lookups with no stamped appId.
|
|
1967
|
+
*/
|
|
1968
|
+
function ownerAppId(reg) {
|
|
1969
|
+
return reg.appId ?? (typeof reg.scope === 'object' ? reg.scope.appId : undefined);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/** Debounce (ms) applied to the typeahead between the user's last keystroke
|
|
1973
|
+
* and the HTTP search. Long enough that a fast typist fires one request, not
|
|
1974
|
+
* one per character. */
|
|
1975
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
1976
|
+
/** Hard cap on candidate rows rendered, regardless of how many the endpoint
|
|
1977
|
+
* returns. The descriptor's `extraParams.pageSize` is the primary bound; this
|
|
1978
|
+
* is a defensive client-side backstop so a misconfigured endpoint can't paint
|
|
1979
|
+
* a thousand-row list. */
|
|
1980
|
+
const MAX_RESULTS_RENDERED = 25;
|
|
1981
|
+
/**
|
|
1982
|
+
* Shared floating entity typeahead — the `/lookup` picker promoted out of the
|
|
1983
|
+
* agent composer into the design-system so any app (agent composer, notes,
|
|
1984
|
+
* task comments, federated remotes) can let a user find an entity and act on
|
|
1985
|
+
* it. Editor-agnostic: it emits a {@link LookupResult} ({@link pick}) and,
|
|
1986
|
+
* when the entity resolves to a deep link, an {@link EntityLinkSelection}
|
|
1987
|
+
* ({@link entityLinkSelected}) — the consumer decides whether to make a ref
|
|
1988
|
+
* chip, insert a `flyos:` link into an editor, etc.
|
|
1989
|
+
*
|
|
1990
|
+
* Two-stage cascade:
|
|
1991
|
+
* - **Stage 1 — entity**: a filterable autocomplete of the offered entities.
|
|
1992
|
+
* Skipped when there's one entity or `initialEntity` already names one.
|
|
1993
|
+
* - **Stage 2 — search**: a debounced typeahead against the chosen entity's
|
|
1994
|
+
* search endpoint, with a breadcrumb back to stage 1.
|
|
1995
|
+
*
|
|
1996
|
+
* The HTTP call goes through the host's `HttpClient` so the gateway routing +
|
|
1997
|
+
* auth interceptor apply — the same path the MCP `*_list_brief` tools wrap,
|
|
1998
|
+
* but user-authenticated. The "exposed by app" badge resolves names via the
|
|
1999
|
+
* optional {@link LOOKUP_APP_NAME_RESOLVER} the host provides (the DS has no
|
|
2000
|
+
* app registry); absent it, the badge falls back to `appBadgeKey`.
|
|
2001
|
+
*/
|
|
2002
|
+
class EntityLookupComponent {
|
|
2003
|
+
http = inject(HttpClient);
|
|
2004
|
+
i18n = inject(I18nService);
|
|
2005
|
+
host = inject((ElementRef));
|
|
2006
|
+
lookupRegistry = inject(AgentLookupRegistry);
|
|
2007
|
+
/** Host-provided appId → {id,name} adapter for the "exposed by app" badge.
|
|
2008
|
+
* The DS has no app registry; the shell (and remotes) provide APP_LOOKUP.
|
|
2009
|
+
* Optional — absent, the badge falls back to a descriptor's appBadgeKey. */
|
|
2010
|
+
appLookup = inject(APP_LOOKUP, { optional: true });
|
|
2011
|
+
/** Entities the picker may search — already filtered to the live-app scope
|
|
2012
|
+
* by the caller. Empty renders a "nothing to look up" hint. */
|
|
2013
|
+
descriptors = input.required(...(ngDevMode ? [{ debugName: "descriptors" }] : /* istanbul ignore next */ []));
|
|
2014
|
+
/** Entity pre-selected (e.g. from `/lookup <entity>`). Ignored when it
|
|
2015
|
+
* doesn't match any descriptor (falls back to the first). */
|
|
2016
|
+
initialEntity = input(undefined, ...(ngDevMode ? [{ debugName: "initialEntity" }] : /* istanbul ignore next */ []));
|
|
2017
|
+
/** Query text pre-seeded (e.g. from `/lookup <entity> <query>`). */
|
|
2018
|
+
initialQuery = input('', ...(ngDevMode ? [{ debugName: "initialQuery" }] : /* istanbul ignore next */ []));
|
|
2019
|
+
/** Per-instance listbox id so `aria-controls` / `aria-activedescendant`
|
|
2020
|
+
* resolve unambiguously when multiple panels mount. */
|
|
2021
|
+
listboxId = input('fly-entity-lookup-listbox', ...(ngDevMode ? [{ debugName: "listboxId" }] : /* istanbul ignore next */ []));
|
|
2022
|
+
/** Open direction. `'up'` (default) suits a bottom-anchored composer (the
|
|
2023
|
+
* agent input); `'down'` suits a top-anchored toolbar button (notes / task
|
|
2024
|
+
* comments) — the panel then anchors to the host's positioned parent (the
|
|
2025
|
+
* button wrapper) and opens below it with a fixed width. */
|
|
2026
|
+
placement = input('up', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
2027
|
+
/** Raw pick — every consumer gets this (agent ref chips, etc.). */
|
|
2028
|
+
pick = output();
|
|
2029
|
+
/** Deep-link selection — emitted ONLY when the picked entity resolves to a
|
|
2030
|
+
* route via the lookup registry (so the consumer never gets a dead link).
|
|
2031
|
+
* Editors bind this to insert a `flyos:` anchor. */
|
|
2032
|
+
entityLinkSelected = output();
|
|
2033
|
+
dismiss = output();
|
|
2034
|
+
/** Which cascade stage is active: pick an entity, then search within it. */
|
|
2035
|
+
stage = signal('entity', ...(ngDevMode ? [{ debugName: "stage" }] : /* istanbul ignore next */ []));
|
|
2036
|
+
/** entity key of the descriptor currently being searched. */
|
|
2037
|
+
activeEntity = signal(null, ...(ngDevMode ? [{ debugName: "activeEntity" }] : /* istanbul ignore next */ []));
|
|
2038
|
+
/** In stage 1 this filters the entity list; in stage 2 it's the search text. */
|
|
2039
|
+
query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
2040
|
+
results = signal([], ...(ngDevMode ? [{ debugName: "results" }] : /* istanbul ignore next */ []));
|
|
2041
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2042
|
+
/** i18n key for an inline error (network / bad endpoint); null when clear. */
|
|
2043
|
+
errorKey = signal(null, ...(ngDevMode ? [{ debugName: "errorKey" }] : /* istanbul ignore next */ []));
|
|
2044
|
+
activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
2045
|
+
/** Query stashed when no entity matched, so it can seed the search field the
|
|
2046
|
+
* moment the user picks an entity. One-shot. */
|
|
2047
|
+
_seedQuery = '';
|
|
2048
|
+
/** The descriptor matching {@link activeEntity}, or null. */
|
|
2049
|
+
activeDescriptor = computed(() => {
|
|
2050
|
+
const e = this.activeEntity();
|
|
2051
|
+
if (!e)
|
|
2052
|
+
return null;
|
|
2053
|
+
return this.descriptors().find((d) => d.entity === e) ?? null;
|
|
2054
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescriptor" }] : /* istanbul ignore next */ []));
|
|
2055
|
+
/** Stage-1 entity options, narrowed by the typed filter. Touches
|
|
2056
|
+
* `i18n.version()` so a locale switch re-resolves the label match. */
|
|
2057
|
+
filteredEntities = computed(() => {
|
|
2058
|
+
this.i18n.version();
|
|
2059
|
+
const q = this.query().trim().toLowerCase();
|
|
2060
|
+
const descs = this.descriptors();
|
|
2061
|
+
if (q.length === 0)
|
|
2062
|
+
return descs;
|
|
2063
|
+
return descs.filter((d) => d.entity.toLowerCase().includes(q) ||
|
|
2064
|
+
this.i18n.t(d.labelKey).toLowerCase().includes(q));
|
|
2065
|
+
}, ...(ngDevMode ? [{ debugName: "filteredEntities" }] : /* istanbul ignore next */ []));
|
|
2066
|
+
/** Size of the list the keyboard currently navigates (entities vs results). */
|
|
2067
|
+
listSize = computed(() => this.stage() === 'entity' ? this.filteredEntities().length : this.results().length, ...(ngDevMode ? [{ debugName: "listSize" }] : /* istanbul ignore next */ []));
|
|
2068
|
+
/** Show the breadcrumb / back affordance only when there's a real choice of
|
|
2069
|
+
* entity to step back to. */
|
|
2070
|
+
showBack = computed(() => this.descriptors().length > 1, ...(ngDevMode ? [{ debugName: "showBack" }] : /* istanbul ignore next */ []));
|
|
2071
|
+
/** Placeholder reflects the active stage. */
|
|
2072
|
+
searchPlaceholderKey = computed(() => this.stage() === 'entity'
|
|
2073
|
+
? 'agent.lookup.entity_placeholder'
|
|
2074
|
+
: 'agent.lookup.search_placeholder', ...(ngDevMode ? [{ debugName: "searchPlaceholderKey" }] : /* istanbul ignore next */ []));
|
|
2075
|
+
_debounceTimer = null;
|
|
2076
|
+
_searchSub = null;
|
|
2077
|
+
searchEl;
|
|
2078
|
+
constructor() {
|
|
2079
|
+
afterNextRender(() => this._focusSearch());
|
|
2080
|
+
}
|
|
2081
|
+
ngOnInit() {
|
|
2082
|
+
const descs = this.descriptors();
|
|
2083
|
+
const initial = this.initialEntity();
|
|
2084
|
+
const chosen = (initial ? descs.find((d) => d.entity === initial) : undefined) ??
|
|
2085
|
+
(descs.length === 1 ? descs[0] : undefined);
|
|
2086
|
+
if (chosen) {
|
|
2087
|
+
this.activeEntity.set(chosen.entity);
|
|
2088
|
+
this.query.set(this.initialQuery() ?? '');
|
|
2089
|
+
this.stage.set('search');
|
|
2090
|
+
this._runSearch();
|
|
2091
|
+
}
|
|
2092
|
+
else {
|
|
2093
|
+
this._seedQuery = this.initialQuery() ?? '';
|
|
2094
|
+
this.activeEntity.set(null);
|
|
2095
|
+
this.query.set('');
|
|
2096
|
+
this.stage.set('entity');
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
ngOnDestroy() {
|
|
2100
|
+
this._clearTimer();
|
|
2101
|
+
this._searchSub?.unsubscribe();
|
|
2102
|
+
}
|
|
2103
|
+
optionId(index) {
|
|
2104
|
+
return `${this.listboxId()}-opt-${index}`;
|
|
2105
|
+
}
|
|
2106
|
+
activeDescendant = computed(() => {
|
|
2107
|
+
if (this.listSize() === 0)
|
|
2108
|
+
return '';
|
|
2109
|
+
return `${this.listboxId()}-opt-${this._clampedIndex()}`;
|
|
2110
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
|
|
2111
|
+
_clampedIndex() {
|
|
2112
|
+
const size = this.listSize();
|
|
2113
|
+
if (size === 0)
|
|
2114
|
+
return 0;
|
|
2115
|
+
return Math.min(Math.max(0, this.activeIndex()), size - 1);
|
|
2116
|
+
}
|
|
2117
|
+
// ── Input + entity handlers ─────────────────────────────────────────────
|
|
2118
|
+
onQueryInput(value) {
|
|
2119
|
+
this.query.set(value);
|
|
2120
|
+
this.activeIndex.set(0);
|
|
2121
|
+
if (this.stage() === 'search')
|
|
2122
|
+
this._scheduleSearch();
|
|
2123
|
+
}
|
|
2124
|
+
onSelectEntity(entity) {
|
|
2125
|
+
this.activeEntity.set(entity);
|
|
2126
|
+
this.activeIndex.set(0);
|
|
2127
|
+
this.stage.set('search');
|
|
2128
|
+
this.query.set(this._seedQuery);
|
|
2129
|
+
this._seedQuery = '';
|
|
2130
|
+
this.errorKey.set(null);
|
|
2131
|
+
this._runSearch();
|
|
2132
|
+
this._focusSearch();
|
|
2133
|
+
}
|
|
2134
|
+
goBackToEntity() {
|
|
2135
|
+
if (!this.showBack())
|
|
2136
|
+
return;
|
|
2137
|
+
this._clearTimer();
|
|
2138
|
+
this._searchSub?.unsubscribe();
|
|
2139
|
+
this.stage.set('entity');
|
|
2140
|
+
this.activeEntity.set(null);
|
|
2141
|
+
this.activeIndex.set(0);
|
|
2142
|
+
this.query.set('');
|
|
2143
|
+
this.results.set([]);
|
|
2144
|
+
this.loading.set(false);
|
|
2145
|
+
this.errorKey.set(null);
|
|
2146
|
+
this._focusSearch();
|
|
2147
|
+
}
|
|
2148
|
+
onRowClick(row) {
|
|
2149
|
+
this._selectRow(row);
|
|
2150
|
+
}
|
|
2151
|
+
onRowHover(index) {
|
|
2152
|
+
if (index !== this.activeIndex())
|
|
2153
|
+
this.activeIndex.set(index);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Single pick exit. Emits the raw {@link pick} for every consumer, AND —
|
|
2157
|
+
* when the entity resolves to a deep-link route via the registry — an
|
|
2158
|
+
* {@link entityLinkSelected} carrying the `flyos:` href. Resolution failures
|
|
2159
|
+
* (unknown entity / app mismatch / owning app not installed / no
|
|
2160
|
+
* `deepLinkRoute`) simply omit the deep-link event: editor consumers get
|
|
2161
|
+
* nothing to insert rather than a dead link, while the raw `pick` path
|
|
2162
|
+
* (agent ref chips) still fires.
|
|
2163
|
+
*/
|
|
2164
|
+
_selectRow(row) {
|
|
2165
|
+
this.pick.emit(row);
|
|
2166
|
+
const target = this.lookupRegistry.resolveDeepLinkForEntity(row.entity, row.id);
|
|
2167
|
+
if (target) {
|
|
2168
|
+
this.entityLinkSelected.emit({
|
|
2169
|
+
label: row.label,
|
|
2170
|
+
kind: `${target.appId}.${row.entity}`,
|
|
2171
|
+
id: row.id,
|
|
2172
|
+
appId: target.appId,
|
|
2173
|
+
route: target.route,
|
|
2174
|
+
href: `flyos:${target.appId}.${row.entity}/${row.id}`,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
onKeydown(ev) {
|
|
2179
|
+
if (ev.key === 'Escape') {
|
|
2180
|
+
ev.preventDefault();
|
|
2181
|
+
ev.stopPropagation();
|
|
2182
|
+
this.dismiss.emit();
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (ev.key === 'Backspace' &&
|
|
2186
|
+
this.stage() === 'search' &&
|
|
2187
|
+
this.query().length === 0 &&
|
|
2188
|
+
this.showBack()) {
|
|
2189
|
+
ev.preventDefault();
|
|
2190
|
+
this.goBackToEntity();
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const size = this.listSize();
|
|
2194
|
+
if (ev.key === 'ArrowDown') {
|
|
2195
|
+
ev.preventDefault();
|
|
2196
|
+
if (size === 0)
|
|
2197
|
+
return;
|
|
2198
|
+
this.activeIndex.set((this._clampedIndex() + 1) % size);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (ev.key === 'ArrowUp') {
|
|
2202
|
+
ev.preventDefault();
|
|
2203
|
+
if (size === 0)
|
|
2204
|
+
return;
|
|
2205
|
+
this.activeIndex.set((this._clampedIndex() - 1 + size) % size);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
if (ev.key === 'Enter') {
|
|
2209
|
+
ev.preventDefault();
|
|
2210
|
+
if (this.stage() === 'entity') {
|
|
2211
|
+
const desc = this.filteredEntities()[this._clampedIndex()];
|
|
2212
|
+
if (desc)
|
|
2213
|
+
this.onSelectEntity(desc.entity);
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
const row = this.results()[this._clampedIndex()];
|
|
2217
|
+
if (row)
|
|
2218
|
+
this._selectRow(row);
|
|
2219
|
+
}
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
onDocumentMouseDown(ev) {
|
|
2224
|
+
const target = ev.target;
|
|
2225
|
+
if (!target)
|
|
2226
|
+
return;
|
|
2227
|
+
if (this.host.nativeElement.contains(target))
|
|
2228
|
+
return;
|
|
2229
|
+
this.dismiss.emit();
|
|
2230
|
+
}
|
|
2231
|
+
onDocumentEscape() {
|
|
2232
|
+
this.dismiss.emit();
|
|
2233
|
+
}
|
|
2234
|
+
// ── Search internals ────────────────────────────────────────────────────
|
|
2235
|
+
_scheduleSearch() {
|
|
2236
|
+
this._clearTimer();
|
|
2237
|
+
this._debounceTimer = setTimeout(() => this._runSearch(), SEARCH_DEBOUNCE_MS);
|
|
2238
|
+
}
|
|
2239
|
+
_clearTimer() {
|
|
2240
|
+
if (this._debounceTimer !== null) {
|
|
2241
|
+
clearTimeout(this._debounceTimer);
|
|
2242
|
+
this._debounceTimer = null;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
_runSearch() {
|
|
2246
|
+
this._clearTimer();
|
|
2247
|
+
const desc = this.activeDescriptor();
|
|
2248
|
+
if (!desc) {
|
|
2249
|
+
this.results.set([]);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const endpoint = desc.search.endpoint;
|
|
2253
|
+
// SSRF / shape guard — relative path only (no scheme/host, no protocol-
|
|
2254
|
+
// relative `//host`).
|
|
2255
|
+
if (!endpoint.startsWith('/') || endpoint.startsWith('//')) {
|
|
2256
|
+
this.errorKey.set('agent.lookup.error');
|
|
2257
|
+
this.results.set([]);
|
|
2258
|
+
this.loading.set(false);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
this._searchSub?.unsubscribe();
|
|
2262
|
+
this.loading.set(true);
|
|
2263
|
+
this.errorKey.set(null);
|
|
2264
|
+
let params = new HttpParams().set(desc.search.queryParam, this.query().trim());
|
|
2265
|
+
for (const [k, v] of Object.entries(desc.search.extraParams ?? {})) {
|
|
2266
|
+
params = params.set(k, v);
|
|
2267
|
+
}
|
|
2268
|
+
this._searchSub = this.http.get(endpoint, { params }).subscribe({
|
|
2269
|
+
next: (body) => {
|
|
2270
|
+
this.results.set(this._mapResults(desc, body));
|
|
2271
|
+
this.loading.set(false);
|
|
2272
|
+
this.activeIndex.set(0);
|
|
2273
|
+
},
|
|
2274
|
+
error: () => {
|
|
2275
|
+
this.errorKey.set('agent.lookup.error');
|
|
2276
|
+
this.results.set([]);
|
|
2277
|
+
this.loading.set(false);
|
|
2278
|
+
},
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
_mapResults(desc, body) {
|
|
2282
|
+
const arr = this._extractArray(body, desc.search.resultsPath);
|
|
2283
|
+
const idField = desc.search.idField ?? 'id';
|
|
2284
|
+
const dispField = desc.search.displayField;
|
|
2285
|
+
const secField = desc.search.secondaryField;
|
|
2286
|
+
const appField = desc.search.appIdField;
|
|
2287
|
+
const out = [];
|
|
2288
|
+
for (const item of arr) {
|
|
2289
|
+
if (!item || typeof item !== 'object')
|
|
2290
|
+
continue;
|
|
2291
|
+
const rec = item;
|
|
2292
|
+
const id = rec[idField];
|
|
2293
|
+
const label = rec[dispField];
|
|
2294
|
+
if (id == null || label == null)
|
|
2295
|
+
continue;
|
|
2296
|
+
const rowAppId = appField != null && rec[appField] != null && rec[appField] !== ''
|
|
2297
|
+
? String(rec[appField])
|
|
2298
|
+
: undefined;
|
|
2299
|
+
out.push({
|
|
2300
|
+
entity: desc.entity,
|
|
2301
|
+
id: String(id),
|
|
2302
|
+
label: String(label),
|
|
2303
|
+
secondary: secField != null && rec[secField] != null
|
|
2304
|
+
? String(rec[secField])
|
|
2305
|
+
: undefined,
|
|
2306
|
+
appId: rowAppId,
|
|
2307
|
+
});
|
|
2308
|
+
if (out.length >= MAX_RESULTS_RENDERED)
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
return out;
|
|
2312
|
+
}
|
|
2313
|
+
_extractArray(body, path) {
|
|
2314
|
+
if (Array.isArray(body))
|
|
2315
|
+
return body;
|
|
2316
|
+
if (!path)
|
|
2317
|
+
return [];
|
|
2318
|
+
let cur = body;
|
|
2319
|
+
for (const seg of path.split('.')) {
|
|
2320
|
+
if (cur && typeof cur === 'object' && seg in cur) {
|
|
2321
|
+
cur = cur[seg];
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
return [];
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return Array.isArray(cur) ? cur : [];
|
|
2328
|
+
}
|
|
2329
|
+
_focusSearch() {
|
|
2330
|
+
queueMicrotask(() => this.searchEl?.nativeElement.focus());
|
|
2331
|
+
}
|
|
2332
|
+
entityLabel(desc) {
|
|
2333
|
+
this.i18n.version();
|
|
2334
|
+
return this.i18n.t(desc.labelKey);
|
|
2335
|
+
}
|
|
2336
|
+
activeEntityLabel = computed(() => {
|
|
2337
|
+
const desc = this.activeDescriptor();
|
|
2338
|
+
return desc ? this.entityLabel(desc) : '';
|
|
2339
|
+
}, ...(ngDevMode ? [{ debugName: "activeEntityLabel" }] : /* istanbul ignore next */ []));
|
|
2340
|
+
/**
|
|
2341
|
+
* Display name of the app exposing this descriptor, for the "exposed by …"
|
|
2342
|
+
* badge. Resolution order: host-provided {@link LOOKUP_APP_NAME_RESOLVER}
|
|
2343
|
+
* (keyed by `descriptor.appId`) → descriptor `appBadgeKey` (literal i18n
|
|
2344
|
+
* key, for entities not mapped to a single shell app) → '' (no badge).
|
|
2345
|
+
*/
|
|
2346
|
+
appLabel(desc) {
|
|
2347
|
+
this.i18n.version();
|
|
2348
|
+
if (desc.appId) {
|
|
2349
|
+
const resolved = this.appLookup?.getById(desc.appId)?.name;
|
|
2350
|
+
if (resolved)
|
|
2351
|
+
return resolved;
|
|
2352
|
+
}
|
|
2353
|
+
if (desc.appBadgeKey)
|
|
2354
|
+
return this.i18n.t(desc.appBadgeKey);
|
|
2355
|
+
return '';
|
|
2356
|
+
}
|
|
2357
|
+
activeAppLabel = computed(() => {
|
|
2358
|
+
const desc = this.activeDescriptor();
|
|
2359
|
+
return desc ? this.appLabel(desc) : '';
|
|
2360
|
+
}, ...(ngDevMode ? [{ debugName: "activeAppLabel" }] : /* istanbul ignore next */ []));
|
|
2361
|
+
/** Per-row source-app label for stage-2 rows. Resolves {@link LookupResult.appId}
|
|
2362
|
+
* through the host resolver. '' when the row carries no `appId`, the resolver
|
|
2363
|
+
* is absent / doesn't know it, or it duplicates {@link activeAppLabel}. */
|
|
2364
|
+
rowAppLabel(row) {
|
|
2365
|
+
this.i18n.version();
|
|
2366
|
+
if (!row.appId)
|
|
2367
|
+
return '';
|
|
2368
|
+
const resolved = this.appLookup?.getById(row.appId)?.name;
|
|
2369
|
+
if (!resolved)
|
|
2370
|
+
return '';
|
|
2371
|
+
if (resolved === this.activeAppLabel())
|
|
2372
|
+
return '';
|
|
2373
|
+
return resolved;
|
|
2374
|
+
}
|
|
2375
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityLookupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2376
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityLookupComponent, isStandalone: true, selector: "fly-entity-lookup", inputs: { descriptors: { classPropertyName: "descriptors", publicName: "descriptors", isSignal: true, isRequired: true, transformFunction: null }, initialEntity: { classPropertyName: "initialEntity", publicName: "initialEntity", isSignal: true, isRequired: false, transformFunction: null }, initialQuery: { classPropertyName: "initialQuery", publicName: "initialQuery", isSignal: true, isRequired: false, transformFunction: null }, listboxId: { classPropertyName: "listboxId", publicName: "listboxId", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pick: "pick", entityLinkSelected: "entityLinkSelected", dismiss: "dismiss" }, host: { listeners: { "document:mousedown": "onDocumentMouseDown($event)", "document:keydown.escape": "onDocumentEscape()" }, properties: { "class.fly-entity-lookup--down": "placement() === 'down'", "class.fly-entity-lookup--above": "placement() === 'above'" } }, viewQueries: [{ propertyName: "searchEl", first: true, predicate: ["searchRef"], descendants: true }], ngImport: i0, template: "<div class=\"fly-entity-lookup\" role=\"dialog\" [attr.aria-label]=\"'agent.lookup.dialog_aria' | translate\">\n @if (descriptors().length === 0) {\n <div class=\"fly-entity-lookup__empty\" role=\"status\">\n {{ 'agent.lookup.no_entities' | translate }}\n </div>\n } @else {\n <div class=\"fly-entity-lookup__search\" role=\"combobox\" aria-haspopup=\"listbox\" aria-expanded=\"true\"\n [attr.aria-controls]=\"listboxId()\">\n @if (stage() === 'search' && showBack()) {\n <!-- Breadcrumb back to the entity picker (also reachable via Backspace\n on an empty query). Shows the chosen entity so the user always\n knows what they're searching within. -->\n <button\n type=\"button\"\n class=\"fly-entity-lookup__crumb\"\n (click)=\"goBackToEntity()\"\n [attr.aria-label]=\"'agent.lookup.back_aria' | translate\">\n <i class=\"pi pi-angle-left fly-entity-lookup__crumb-back\" aria-hidden=\"true\"></i>\n @if (activeDescriptor()?.icon; as ic) {\n <i class=\"pi {{ ic }}\" aria-hidden=\"true\"></i>\n }\n <span>{{ activeEntityLabel() }}</span>\n </button>\n } @else {\n <i class=\"pi pi-search fly-entity-lookup__search-icon\" aria-hidden=\"true\"></i>\n }\n <input\n #searchRef\n type=\"text\"\n class=\"fly-entity-lookup__search-input\"\n [ngModel]=\"query()\"\n (ngModelChange)=\"onQueryInput($event)\"\n (keydown)=\"onKeydown($event)\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"listboxId()\"\n [placeholder]=\"searchPlaceholderKey() | translate\" />\n </div>\n\n <div [id]=\"listboxId()\" class=\"fly-entity-lookup__results\" role=\"listbox\"\n [attr.aria-label]=\"(stage() === 'entity' ? 'agent.lookup.entities_aria' : 'agent.lookup.results_aria') | translate\">\n @if (stage() === 'entity') {\n <!-- Stage 1: entity autocomplete. -->\n @if (filteredEntities().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (desc of filteredEntities(); track desc.entity; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt fly-entity-lookup__opt--entity\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onSelectEntity(desc.entity)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\">\n @if (desc.icon) {\n <i class=\"pi {{ desc.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ entityLabel(desc) }}\n </span>\n @if (appLabel(desc); as app) {\n <span class=\"fly-entity-lookup__opt-app-badge\" [title]=\"app\">{{ app }}</span>\n }\n <i class=\"pi pi-angle-right fly-entity-lookup__opt-chevron\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n } @else {\n <!-- Stage 2: result typeahead within the chosen entity. -->\n @if (loading()) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.searching' | translate }}</div>\n } @else if (errorKey(); as err) {\n <div class=\"fly-entity-lookup__status fly-entity-lookup__status--error\" role=\"alert\">{{ err | translate }}</div>\n } @else if (results().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (row of results(); track row.id; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onRowClick(row)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\" [title]=\"row.label\">{{ row.label }}</span>\n @if (row.secondary) {\n <span class=\"fly-entity-lookup__opt-secondary\">{{ row.secondary }}</span>\n }\n @if (rowAppLabel(row); as rowApp) {\n <!-- Source app of this individual row (e.g. \"Circles\" for a\n Help-Center article seeded by Circles). Rendered before the\n descriptor tag so the per-row signal sits closer to the\n title; suppressed when it would duplicate the descriptor\n label. -->\n <span class=\"fly-entity-lookup__opt-app-tag fly-entity-lookup__opt-app-tag--source\"\n [title]=\"rowApp\">{{ rowApp }}</span>\n }\n @if (activeAppLabel(); as app) {\n <span class=\"fly-entity-lookup__opt-app-tag\" [title]=\"app\">{{ app }}</span>\n }\n </button>\n }\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative;z-index:6}:host(.fly-entity-lookup--down){position:static}:host(.fly-entity-lookup--down) .fly-entity-lookup{inset-block-end:auto;inset-block-start:100%;inset-inline:auto;inset-inline-end:0;margin-block-end:0;margin-block-start:6px;min-inline-size:320px;max-inline-size:min(380px,90vw);z-index:1000}:host(.fly-entity-lookup--above){position:static}:host(.fly-entity-lookup--above) .fly-entity-lookup{inset-block-start:auto;inset-block-end:100%;inset-inline:0;margin-block-start:0;margin-block-end:6px;z-index:1000}.fly-entity-lookup{position:absolute;inset-block-end:100%;inset-inline:12px;margin-block-end:6px;background:var(--surface-card, rgba(28, 28, 30, .96));backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);border:1px solid var(--surface-border, rgba(255, 255, 255, .1));border-radius:12px;box-shadow:0 10px 24px #0000002e}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.fly-entity-lookup{background:#1c1c1efa}}.fly-entity-lookup{padding-block:6px;display:flex;flex-direction:column;gap:6px}.fly-entity-lookup__empty{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__crumb{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;max-inline-size:45%;padding-block:2px;padding-inline:7px;border-radius:6px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .06));color:var(--text-color-secondary, rgba(255, 255, 255, .75));font-size:11px;font-family:inherit;line-height:1.2;cursor:pointer;transition:background .12s ease,color .12s ease}.fly-entity-lookup__crumb i.pi{font-size:12px}.fly-entity-lookup__crumb .fly-entity-lookup__crumb-back{opacity:.75}.fly-entity-lookup__crumb span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__crumb:hover,.fly-entity-lookup__crumb:focus-visible{background:var(--surface-active, rgba(255, 255, 255, .1));color:inherit;outline:none}.fly-entity-lookup__crumb:focus-visible{outline:2px solid var(--primary-color, #5b8cff);outline-offset:1px}[dir=rtl] .fly-entity-lookup__crumb-back{transform:scaleX(-1)}.fly-entity-lookup__search{display:flex;align-items:center;gap:8px;margin-inline:10px;padding-block:6px;padding-inline:10px;border-radius:8px;background:var(--surface-hover, rgba(255, 255, 255, .06));border:1px solid var(--surface-border, rgba(255, 255, 255, .1))}.fly-entity-lookup__search-icon{color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:13px}.fly-entity-lookup__search-input{flex:1 1 auto;min-inline-size:0;background:transparent;border:none;outline:none;color:inherit;font-size:13px;font-family:inherit}.fly-entity-lookup__search-input::placeholder{color:var(--text-color-secondary, rgba(255, 255, 255, .4))}.fly-entity-lookup__results{max-block-size:240px;overflow-y:auto;display:flex;flex-direction:column}.fly-entity-lookup__status{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__status--error{color:var(--red-400, #f87171)}.fly-entity-lookup__opt{display:flex;align-items:center;gap:8px;padding-block:6px;padding-inline:14px;background:transparent;border:none;text-align:start;color:inherit;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s ease}.fly-entity-lookup__opt.is-active,.fly-entity-lookup__opt:focus-visible{background:var(--surface-hover, rgba(255, 255, 255, .08));outline:none}.fly-entity-lookup__opt-label{flex:1 1 auto;min-inline-size:0;display:inline-flex;align-items:center;gap:7px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-label i.pi{font-size:13px;opacity:.85;flex-shrink:0}.fly-entity-lookup__opt-secondary{flex-shrink:0;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-color-secondary, rgba(255, 255, 255, .5));white-space:nowrap}.fly-entity-lookup__opt-app-badge{flex-shrink:0;padding-block:1px;padding-inline:7px;border-radius:999px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .05));color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-size:10.5px;line-height:1.5;letter-spacing:.02em;white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag{flex-shrink:0;font-size:10.5px;color:var(--text-color-secondary, rgba(255, 255, 255, .4));white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag--source{color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-weight:500}.fly-entity-lookup__opt-chevron{flex-shrink:0;font-size:12px;opacity:.4}.fly-entity-lookup__opt--entity.is-active .fly-entity-lookup__opt-chevron,.fly-entity-lookup__opt--entity:hover .fly-entity-lookup__opt-chevron{opacity:.75}[dir=rtl] .fly-entity-lookup__opt-chevron{transform:scaleX(-1)}@media(prefers-reduced-motion:reduce){.fly-entity-lookup__crumb,.fly-entity-lookup__opt{transition:none}}@media(forced-colors:active){.fly-entity-lookup{border-color:CanvasText}.fly-entity-lookup__opt.is-active{outline:1px solid Highlight;outline-offset:-1px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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.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 });
|
|
2377
|
+
}
|
|
2378
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityLookupComponent, decorators: [{
|
|
2379
|
+
type: Component,
|
|
2380
|
+
args: [{ selector: 'fly-entity-lookup', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2381
|
+
'[class.fly-entity-lookup--down]': "placement() === 'down'",
|
|
2382
|
+
'[class.fly-entity-lookup--above]': "placement() === 'above'",
|
|
2383
|
+
}, template: "<div class=\"fly-entity-lookup\" role=\"dialog\" [attr.aria-label]=\"'agent.lookup.dialog_aria' | translate\">\n @if (descriptors().length === 0) {\n <div class=\"fly-entity-lookup__empty\" role=\"status\">\n {{ 'agent.lookup.no_entities' | translate }}\n </div>\n } @else {\n <div class=\"fly-entity-lookup__search\" role=\"combobox\" aria-haspopup=\"listbox\" aria-expanded=\"true\"\n [attr.aria-controls]=\"listboxId()\">\n @if (stage() === 'search' && showBack()) {\n <!-- Breadcrumb back to the entity picker (also reachable via Backspace\n on an empty query). Shows the chosen entity so the user always\n knows what they're searching within. -->\n <button\n type=\"button\"\n class=\"fly-entity-lookup__crumb\"\n (click)=\"goBackToEntity()\"\n [attr.aria-label]=\"'agent.lookup.back_aria' | translate\">\n <i class=\"pi pi-angle-left fly-entity-lookup__crumb-back\" aria-hidden=\"true\"></i>\n @if (activeDescriptor()?.icon; as ic) {\n <i class=\"pi {{ ic }}\" aria-hidden=\"true\"></i>\n }\n <span>{{ activeEntityLabel() }}</span>\n </button>\n } @else {\n <i class=\"pi pi-search fly-entity-lookup__search-icon\" aria-hidden=\"true\"></i>\n }\n <input\n #searchRef\n type=\"text\"\n class=\"fly-entity-lookup__search-input\"\n [ngModel]=\"query()\"\n (ngModelChange)=\"onQueryInput($event)\"\n (keydown)=\"onKeydown($event)\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"listboxId()\"\n [placeholder]=\"searchPlaceholderKey() | translate\" />\n </div>\n\n <div [id]=\"listboxId()\" class=\"fly-entity-lookup__results\" role=\"listbox\"\n [attr.aria-label]=\"(stage() === 'entity' ? 'agent.lookup.entities_aria' : 'agent.lookup.results_aria') | translate\">\n @if (stage() === 'entity') {\n <!-- Stage 1: entity autocomplete. -->\n @if (filteredEntities().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (desc of filteredEntities(); track desc.entity; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt fly-entity-lookup__opt--entity\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onSelectEntity(desc.entity)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\">\n @if (desc.icon) {\n <i class=\"pi {{ desc.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ entityLabel(desc) }}\n </span>\n @if (appLabel(desc); as app) {\n <span class=\"fly-entity-lookup__opt-app-badge\" [title]=\"app\">{{ app }}</span>\n }\n <i class=\"pi pi-angle-right fly-entity-lookup__opt-chevron\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n } @else {\n <!-- Stage 2: result typeahead within the chosen entity. -->\n @if (loading()) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.searching' | translate }}</div>\n } @else if (errorKey(); as err) {\n <div class=\"fly-entity-lookup__status fly-entity-lookup__status--error\" role=\"alert\">{{ err | translate }}</div>\n } @else if (results().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (row of results(); track row.id; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onRowClick(row)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\" [title]=\"row.label\">{{ row.label }}</span>\n @if (row.secondary) {\n <span class=\"fly-entity-lookup__opt-secondary\">{{ row.secondary }}</span>\n }\n @if (rowAppLabel(row); as rowApp) {\n <!-- Source app of this individual row (e.g. \"Circles\" for a\n Help-Center article seeded by Circles). Rendered before the\n descriptor tag so the per-row signal sits closer to the\n title; suppressed when it would duplicate the descriptor\n label. -->\n <span class=\"fly-entity-lookup__opt-app-tag fly-entity-lookup__opt-app-tag--source\"\n [title]=\"rowApp\">{{ rowApp }}</span>\n }\n @if (activeAppLabel(); as app) {\n <span class=\"fly-entity-lookup__opt-app-tag\" [title]=\"app\">{{ app }}</span>\n }\n </button>\n }\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative;z-index:6}:host(.fly-entity-lookup--down){position:static}:host(.fly-entity-lookup--down) .fly-entity-lookup{inset-block-end:auto;inset-block-start:100%;inset-inline:auto;inset-inline-end:0;margin-block-end:0;margin-block-start:6px;min-inline-size:320px;max-inline-size:min(380px,90vw);z-index:1000}:host(.fly-entity-lookup--above){position:static}:host(.fly-entity-lookup--above) .fly-entity-lookup{inset-block-start:auto;inset-block-end:100%;inset-inline:0;margin-block-start:0;margin-block-end:6px;z-index:1000}.fly-entity-lookup{position:absolute;inset-block-end:100%;inset-inline:12px;margin-block-end:6px;background:var(--surface-card, rgba(28, 28, 30, .96));backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);border:1px solid var(--surface-border, rgba(255, 255, 255, .1));border-radius:12px;box-shadow:0 10px 24px #0000002e}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.fly-entity-lookup{background:#1c1c1efa}}.fly-entity-lookup{padding-block:6px;display:flex;flex-direction:column;gap:6px}.fly-entity-lookup__empty{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__crumb{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;max-inline-size:45%;padding-block:2px;padding-inline:7px;border-radius:6px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .06));color:var(--text-color-secondary, rgba(255, 255, 255, .75));font-size:11px;font-family:inherit;line-height:1.2;cursor:pointer;transition:background .12s ease,color .12s ease}.fly-entity-lookup__crumb i.pi{font-size:12px}.fly-entity-lookup__crumb .fly-entity-lookup__crumb-back{opacity:.75}.fly-entity-lookup__crumb span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__crumb:hover,.fly-entity-lookup__crumb:focus-visible{background:var(--surface-active, rgba(255, 255, 255, .1));color:inherit;outline:none}.fly-entity-lookup__crumb:focus-visible{outline:2px solid var(--primary-color, #5b8cff);outline-offset:1px}[dir=rtl] .fly-entity-lookup__crumb-back{transform:scaleX(-1)}.fly-entity-lookup__search{display:flex;align-items:center;gap:8px;margin-inline:10px;padding-block:6px;padding-inline:10px;border-radius:8px;background:var(--surface-hover, rgba(255, 255, 255, .06));border:1px solid var(--surface-border, rgba(255, 255, 255, .1))}.fly-entity-lookup__search-icon{color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:13px}.fly-entity-lookup__search-input{flex:1 1 auto;min-inline-size:0;background:transparent;border:none;outline:none;color:inherit;font-size:13px;font-family:inherit}.fly-entity-lookup__search-input::placeholder{color:var(--text-color-secondary, rgba(255, 255, 255, .4))}.fly-entity-lookup__results{max-block-size:240px;overflow-y:auto;display:flex;flex-direction:column}.fly-entity-lookup__status{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__status--error{color:var(--red-400, #f87171)}.fly-entity-lookup__opt{display:flex;align-items:center;gap:8px;padding-block:6px;padding-inline:14px;background:transparent;border:none;text-align:start;color:inherit;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s ease}.fly-entity-lookup__opt.is-active,.fly-entity-lookup__opt:focus-visible{background:var(--surface-hover, rgba(255, 255, 255, .08));outline:none}.fly-entity-lookup__opt-label{flex:1 1 auto;min-inline-size:0;display:inline-flex;align-items:center;gap:7px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-label i.pi{font-size:13px;opacity:.85;flex-shrink:0}.fly-entity-lookup__opt-secondary{flex-shrink:0;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-color-secondary, rgba(255, 255, 255, .5));white-space:nowrap}.fly-entity-lookup__opt-app-badge{flex-shrink:0;padding-block:1px;padding-inline:7px;border-radius:999px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .05));color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-size:10.5px;line-height:1.5;letter-spacing:.02em;white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag{flex-shrink:0;font-size:10.5px;color:var(--text-color-secondary, rgba(255, 255, 255, .4));white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag--source{color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-weight:500}.fly-entity-lookup__opt-chevron{flex-shrink:0;font-size:12px;opacity:.4}.fly-entity-lookup__opt--entity.is-active .fly-entity-lookup__opt-chevron,.fly-entity-lookup__opt--entity:hover .fly-entity-lookup__opt-chevron{opacity:.75}[dir=rtl] .fly-entity-lookup__opt-chevron{transform:scaleX(-1)}@media(prefers-reduced-motion:reduce){.fly-entity-lookup__crumb,.fly-entity-lookup__opt{transition:none}}@media(forced-colors:active){.fly-entity-lookup{border-color:CanvasText}.fly-entity-lookup__opt.is-active{outline:1px solid Highlight;outline-offset:-1px}}\n"] }]
|
|
2384
|
+
}], ctorParameters: () => [], propDecorators: { descriptors: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptors", required: true }] }], initialEntity: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialEntity", required: false }] }], initialQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialQuery", required: false }] }], listboxId: [{ type: i0.Input, args: [{ isSignal: true, alias: "listboxId", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], pick: [{ type: i0.Output, args: ["pick"] }], entityLinkSelected: [{ type: i0.Output, args: ["entityLinkSelected"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], searchEl: [{
|
|
2385
|
+
type: ViewChild,
|
|
2386
|
+
args: ['searchRef']
|
|
2387
|
+
}], onDocumentMouseDown: [{
|
|
2388
|
+
type: HostListener,
|
|
2389
|
+
args: ['document:mousedown', ['$event']]
|
|
2390
|
+
}], onDocumentEscape: [{
|
|
2391
|
+
type: HostListener,
|
|
2392
|
+
args: ['document:keydown.escape']
|
|
2393
|
+
}] } });
|
|
2394
|
+
|
|
2395
|
+
const ENTITY_LINK_LAUNCHER = new InjectionToken('ENTITY_LINK_LAUNCHER');
|
|
2396
|
+
const MARKDOWN_TOOLBAR_PRESETS = {
|
|
2397
|
+
full: [
|
|
2398
|
+
'undo', 'redo', '|',
|
|
2399
|
+
'h1', 'h2', 'h3', '|',
|
|
2400
|
+
'bold', 'italic', 'underline', 'strike', 'code', '|',
|
|
2401
|
+
'bulletList', 'orderedList', 'taskList', '|',
|
|
2402
|
+
'blockquote', 'codeBlock', 'horizontalRule', '|',
|
|
2403
|
+
'link', 'entityLink',
|
|
2404
|
+
],
|
|
2405
|
+
compact: [
|
|
2406
|
+
'bold', 'italic', 'code', '|',
|
|
2407
|
+
'bulletList', 'orderedList', '|',
|
|
2408
|
+
'link', 'entityLink',
|
|
2409
|
+
],
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
/** Parses a live `flyos:<appId>.<entity>/<id>` href into `{kind, id}`. */
|
|
2413
|
+
const FLYOS_HREF_RE = /^flyos:([^/]+)\/(.+)$/;
|
|
2414
|
+
/** Simple command buttons (everything except link / entityLink / divider).
|
|
2415
|
+
* `cmd` runs on the focused chain; `active` is the `isActive` probe. */
|
|
2416
|
+
const TOOLBAR_BUTTONS = {
|
|
2417
|
+
undo: { icon: 'pi-undo', labelKey: 'common.label.undo', cmd: (e) => e.commands.undo() },
|
|
2418
|
+
redo: { icon: 'pi-refresh', labelKey: 'common.label.redo', cmd: (e) => e.commands.redo() },
|
|
2419
|
+
bold: { icon: '', labelKey: 'common.label.bold', cmd: (e) => e.chain().focus().toggleBold().run(), active: { name: 'bold' } },
|
|
2420
|
+
italic: { icon: '', labelKey: 'common.label.italic', cmd: (e) => e.chain().focus().toggleItalic().run(), active: { name: 'italic' } },
|
|
2421
|
+
underline: { icon: '', labelKey: 'common.label.underline', cmd: (e) => e.chain().focus().toggleUnderline().run(), active: { name: 'underline' } },
|
|
2422
|
+
strike: { icon: '', labelKey: 'common.label.strikethrough', cmd: (e) => e.chain().focus().toggleStrike().run(), active: { name: 'strike' } },
|
|
2423
|
+
code: { icon: 'pi-code', labelKey: 'common.label.inline_code', cmd: (e) => e.chain().focus().toggleCode().run(), active: { name: 'code' } },
|
|
2424
|
+
codeBlock: { icon: 'pi-file-edit', labelKey: 'common.label.code_block', cmd: (e) => e.chain().focus().toggleCodeBlock().run(), active: { name: 'codeBlock' } },
|
|
2425
|
+
blockquote: { icon: 'pi-comment', labelKey: 'common.label.blockquote', cmd: (e) => e.chain().focus().toggleBlockquote().run(), active: { name: 'blockquote' } },
|
|
2426
|
+
bulletList: { icon: 'pi-list', labelKey: 'common.label.bullet_list', cmd: (e) => e.chain().focus().toggleBulletList().run(), active: { name: 'bulletList' } },
|
|
2427
|
+
orderedList: { icon: 'pi-sort-numeric-down', labelKey: 'common.label.numbered_list', cmd: (e) => e.chain().focus().toggleOrderedList().run(), active: { name: 'orderedList' } },
|
|
2428
|
+
taskList: { icon: 'pi-check-square', labelKey: 'common.label.task_list', cmd: (e) => e.chain().focus().toggleTaskList().run(), active: { name: 'taskList' } },
|
|
2429
|
+
horizontalRule: { icon: 'pi-minus', labelKey: 'common.label.horizontal_rule', cmd: (e) => e.chain().focus().setHorizontalRule().run() },
|
|
2430
|
+
h1: { icon: '', labelKey: 'common.label.heading1', cmd: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(), active: { name: 'heading', attrs: { level: 1 } } },
|
|
2431
|
+
h2: { icon: '', labelKey: 'common.label.heading2', cmd: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(), active: { name: 'heading', attrs: { level: 2 } } },
|
|
2432
|
+
h3: { icon: '', labelKey: 'common.label.heading3', cmd: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(), active: { name: 'heading', attrs: { level: 3 } } },
|
|
2433
|
+
};
|
|
2434
|
+
/**
|
|
2435
|
+
* Shared rich-text/markdown editor — the single Tiptap-backed editor for FlyOS
|
|
2436
|
+
* apps (notes, task comments, admin) and Business Apps (Circles). Emits a
|
|
2437
|
+
* Markdown string via `ControlValueAccessor`, so it slots into reactive forms
|
|
2438
|
+
* or `[(ngModel)]` with no orchestration.
|
|
2439
|
+
*
|
|
2440
|
+
* Bakes in the platform's `flyos:` entity-deep-link contract: the Link mark
|
|
2441
|
+
* whitelists the `flyos` scheme (so authored/pasted deep links aren't blanked),
|
|
2442
|
+
* clicks on a resolvable `flyos:` anchor launch the owning app via the
|
|
2443
|
+
* host-provided {@link ENTITY_LINK_LAUNCHER}, and the optional "entity link"
|
|
2444
|
+
* toolbar button opens the shared {@link EntityLookupComponent} to insert one.
|
|
2445
|
+
*/
|
|
2446
|
+
class FlyMarkdownEditorComponent {
|
|
2447
|
+
cdr = inject(ChangeDetectorRef);
|
|
2448
|
+
lookupRegistry = inject(AgentLookupRegistry);
|
|
2449
|
+
launcher = inject(ENTITY_LINK_LAUNCHER, { optional: true });
|
|
2450
|
+
/** Toolbar preset name or an explicit ordered item list. */
|
|
2451
|
+
toolbar = input('full', ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
|
|
2452
|
+
/** Show the "insert in-app link" (entity-lookup) toolbar button. */
|
|
2453
|
+
enableEntityLink = input(true, ...(ngDevMode ? [{ debugName: "enableEntityLink" }] : /* istanbul ignore next */ []));
|
|
2454
|
+
/** Direction the entity-lookup dropdown opens. `down` (default) suits a
|
|
2455
|
+
* top toolbar (notes); `above` suits a bottom-anchored composer (task
|
|
2456
|
+
* comments) so the panel doesn't run off the viewport. */
|
|
2457
|
+
entityLinkPlacement = input('down', ...(ngDevMode ? [{ debugName: "entityLinkPlacement" }] : /* istanbul ignore next */ []));
|
|
2458
|
+
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
2459
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
2460
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
2461
|
+
/** Fired on Ctrl/Cmd+Enter (host can submit a comment, etc.). */
|
|
2462
|
+
submitShortcut = output();
|
|
2463
|
+
// CVA plumbing.
|
|
2464
|
+
onChange = () => undefined;
|
|
2465
|
+
onTouched = () => undefined;
|
|
2466
|
+
currentValue = '';
|
|
2467
|
+
// Toolbar reactive state.
|
|
2468
|
+
activeStates = signal({}, ...(ngDevMode ? [{ debugName: "activeStates" }] : /* istanbul ignore next */ []));
|
|
2469
|
+
canUndo = signal(false, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
|
|
2470
|
+
canRedo = signal(false, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
|
|
2471
|
+
// Link (URL) popover.
|
|
2472
|
+
showLinkPopover = signal(false, ...(ngDevMode ? [{ debugName: "showLinkPopover" }] : /* istanbul ignore next */ []));
|
|
2473
|
+
linkInputValue = signal('', ...(ngDevMode ? [{ debugName: "linkInputValue" }] : /* istanbul ignore next */ []));
|
|
2474
|
+
// Entity-link picker.
|
|
2475
|
+
showEntityLookup = signal(false, ...(ngDevMode ? [{ debugName: "showEntityLookup" }] : /* istanbul ignore next */ []));
|
|
2476
|
+
entityLookupDescriptors = computed(() => this.lookupRegistry.all().filter((d) => !!d.deepLinkRoute), ...(ngDevMode ? [{ debugName: "entityLookupDescriptors" }] : /* istanbul ignore next */ []));
|
|
2477
|
+
/** Resolved toolbar items (preset → array), entityLink stripped when disabled. */
|
|
2478
|
+
toolbarItems = computed(() => {
|
|
2479
|
+
const t = this.toolbar();
|
|
2480
|
+
const items = typeof t === 'string' ? MARKDOWN_TOOLBAR_PRESETS[t] : t;
|
|
2481
|
+
return this.enableEntityLink() ? items : items.filter((i) => i !== 'entityLink');
|
|
2482
|
+
}, ...(ngDevMode ? [{ debugName: "toolbarItems" }] : /* istanbul ignore next */ []));
|
|
2483
|
+
editor;
|
|
2484
|
+
constructor() {
|
|
2485
|
+
this.editor = new Editor({
|
|
2486
|
+
extensions: [
|
|
2487
|
+
StarterKit.configure({
|
|
2488
|
+
// Whitelist the platform deep-link scheme so Tiptap keeps the href
|
|
2489
|
+
// instead of blanking it; openOnClick false because flyos: is
|
|
2490
|
+
// non-routable — handleClick launches in-shell below.
|
|
2491
|
+
link: { openOnClick: false, protocols: ['flyos'] },
|
|
2492
|
+
}),
|
|
2493
|
+
Markdown,
|
|
2494
|
+
TaskList,
|
|
2495
|
+
TaskItem.configure({ nested: true }),
|
|
2496
|
+
],
|
|
2497
|
+
editorProps: {
|
|
2498
|
+
attributes: { class: 'fly-md-editor__prosemirror', spellcheck: 'true' },
|
|
2499
|
+
handleClick: (_view, _pos, event) => this.handleFlyosClick(event),
|
|
2500
|
+
handleKeyDown: (_view, event) => {
|
|
2501
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
2502
|
+
event.preventDefault();
|
|
2503
|
+
this.submitShortcut.emit();
|
|
2504
|
+
return true;
|
|
2505
|
+
}
|
|
2506
|
+
return false;
|
|
2507
|
+
},
|
|
2508
|
+
},
|
|
2509
|
+
onUpdate: () => {
|
|
2510
|
+
this.currentValue = this.editor.getMarkdown();
|
|
2511
|
+
this.onChange(this.currentValue);
|
|
2512
|
+
this.refreshState();
|
|
2513
|
+
this.cdr.markForCheck();
|
|
2514
|
+
},
|
|
2515
|
+
onSelectionUpdate: () => {
|
|
2516
|
+
this.refreshState();
|
|
2517
|
+
this.cdr.markForCheck();
|
|
2518
|
+
},
|
|
2519
|
+
onBlur: () => this.onTouched(),
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
ngOnDestroy() {
|
|
2523
|
+
this.editor.destroy();
|
|
2524
|
+
}
|
|
2525
|
+
// ── ControlValueAccessor ────────────────────────────────────────────────
|
|
2526
|
+
writeValue(value) {
|
|
2527
|
+
this.currentValue = value ?? '';
|
|
2528
|
+
// contentType:'markdown' parses the string as Markdown; emitUpdate:false so
|
|
2529
|
+
// hydrating the model doesn't echo back through onChange.
|
|
2530
|
+
this.editor.commands.setContent(this.currentValue, { emitUpdate: false, contentType: 'markdown' });
|
|
2531
|
+
this.refreshState();
|
|
2532
|
+
this.cdr.markForCheck();
|
|
2533
|
+
}
|
|
2534
|
+
registerOnChange(fn) { this.onChange = fn; }
|
|
2535
|
+
registerOnTouched(fn) { this.onTouched = fn; }
|
|
2536
|
+
setDisabledState(isDisabled) { this.editor.setEditable(!isDisabled); }
|
|
2537
|
+
// ── Imperative reads (for hosts that persist a rendered HTML snapshot
|
|
2538
|
+
// alongside the markdown CVA value, e.g. notes' contentHtml) ───────────
|
|
2539
|
+
/** Current markdown (same as the CVA value). */
|
|
2540
|
+
getMarkdown() { return this.editor.getMarkdown(); }
|
|
2541
|
+
/** Current rendered HTML snapshot. */
|
|
2542
|
+
getHtml() { return this.editor.getHTML(); }
|
|
2543
|
+
/** Focus the editor surface. */
|
|
2544
|
+
focus() { this.editor.commands.focus(); }
|
|
2545
|
+
// ── Toolbar ──────────────────────────────────────────────────────────────
|
|
2546
|
+
button(id) {
|
|
2547
|
+
const b = TOOLBAR_BUTTONS[id];
|
|
2548
|
+
return b ? { icon: b.icon, labelKey: b.labelKey } : null;
|
|
2549
|
+
}
|
|
2550
|
+
/** Short visual label for buttons rendered as text (B / I / U / S / H1…). */
|
|
2551
|
+
textLabel(id) {
|
|
2552
|
+
switch (id) {
|
|
2553
|
+
case 'bold': return 'B';
|
|
2554
|
+
case 'italic': return 'I';
|
|
2555
|
+
case 'underline': return 'U';
|
|
2556
|
+
case 'strike': return 'S';
|
|
2557
|
+
case 'h1': return 'H1';
|
|
2558
|
+
case 'h2': return 'H2';
|
|
2559
|
+
case 'h3': return 'H3';
|
|
2560
|
+
default: return null;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
run(id) {
|
|
2564
|
+
const b = TOOLBAR_BUTTONS[id];
|
|
2565
|
+
if (!b)
|
|
2566
|
+
return;
|
|
2567
|
+
b.cmd(this.editor);
|
|
2568
|
+
this.refreshState();
|
|
2569
|
+
this.cdr.markForCheck();
|
|
2570
|
+
}
|
|
2571
|
+
isActive(id) {
|
|
2572
|
+
return this.activeStates()[id] ?? false;
|
|
2573
|
+
}
|
|
2574
|
+
disabled(id) {
|
|
2575
|
+
if (id === 'undo')
|
|
2576
|
+
return !this.canUndo();
|
|
2577
|
+
if (id === 'redo')
|
|
2578
|
+
return !this.canRedo();
|
|
2579
|
+
return false;
|
|
2580
|
+
}
|
|
2581
|
+
refreshState() {
|
|
2582
|
+
this.canUndo.set(this.editor.can().undo());
|
|
2583
|
+
this.canRedo.set(this.editor.can().redo());
|
|
2584
|
+
const states = {};
|
|
2585
|
+
for (const [id, b] of Object.entries(TOOLBAR_BUTTONS)) {
|
|
2586
|
+
if (b.active)
|
|
2587
|
+
states[id] = this.editor.isActive(b.active.name, b.active.attrs ?? {});
|
|
2588
|
+
}
|
|
2589
|
+
states['link'] = this.editor.isActive('link');
|
|
2590
|
+
this.activeStates.set(states);
|
|
2591
|
+
}
|
|
2592
|
+
// ── Link (URL) popover ─────────────────────────────────────────────────
|
|
2593
|
+
openLinkPopover() {
|
|
2594
|
+
this.showEntityLookup.set(false);
|
|
2595
|
+
this.linkInputValue.set(this.editor.getAttributes('link')['href'] ?? '');
|
|
2596
|
+
this.showLinkPopover.set(true);
|
|
2597
|
+
}
|
|
2598
|
+
onLinkInput(value) { this.linkInputValue.set(value); }
|
|
2599
|
+
commitLink() {
|
|
2600
|
+
const url = this.linkInputValue().trim();
|
|
2601
|
+
const chain = this.editor.chain().focus().extendMarkRange('link');
|
|
2602
|
+
if (url === '')
|
|
2603
|
+
chain.unsetLink().run();
|
|
2604
|
+
else
|
|
2605
|
+
chain.setLink({ href: url }).run();
|
|
2606
|
+
this.showLinkPopover.set(false);
|
|
2607
|
+
this.refreshState();
|
|
2608
|
+
this.cdr.markForCheck();
|
|
2609
|
+
}
|
|
2610
|
+
cancelLink() { this.showLinkPopover.set(false); }
|
|
2611
|
+
onLinkKeydown(event) {
|
|
2612
|
+
if (event.key === 'Enter') {
|
|
2613
|
+
event.preventDefault();
|
|
2614
|
+
this.commitLink();
|
|
2615
|
+
}
|
|
2616
|
+
if (event.key === 'Escape') {
|
|
2617
|
+
this.cancelLink();
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
// ── Entity-link picker ─────────────────────────────────────────────────
|
|
2621
|
+
toggleEntityLookup() {
|
|
2622
|
+
this.showLinkPopover.set(false);
|
|
2623
|
+
this.showEntityLookup.update((open) => !open);
|
|
2624
|
+
}
|
|
2625
|
+
closeEntityLookup() { this.showEntityLookup.set(false); }
|
|
2626
|
+
onEntityLinkSelected(sel) {
|
|
2627
|
+
this.editor
|
|
2628
|
+
.chain()
|
|
2629
|
+
.focus()
|
|
2630
|
+
.insertContent({ type: 'text', text: sel.label, marks: [{ type: 'link', attrs: { href: sel.href } }] })
|
|
2631
|
+
.insertContent(' ')
|
|
2632
|
+
.run();
|
|
2633
|
+
this.showEntityLookup.set(false);
|
|
2634
|
+
this.refreshState();
|
|
2635
|
+
this.cdr.markForCheck();
|
|
2636
|
+
}
|
|
2637
|
+
// ── flyos: click → launch ──────────────────────────────────────────────
|
|
2638
|
+
/** True (click handled) when a resolvable `flyos:` anchor was launched. */
|
|
2639
|
+
handleFlyosClick(event) {
|
|
2640
|
+
const anchor = event.target?.closest('a');
|
|
2641
|
+
const href = anchor?.getAttribute('href');
|
|
2642
|
+
if (!href)
|
|
2643
|
+
return false;
|
|
2644
|
+
const m = FLYOS_HREF_RE.exec(href);
|
|
2645
|
+
if (!m)
|
|
2646
|
+
return false;
|
|
2647
|
+
event.preventDefault();
|
|
2648
|
+
const [, kind, id] = m;
|
|
2649
|
+
const target = this.lookupRegistry.resolveDeepLink(kind, id);
|
|
2650
|
+
if (!target || !this.launcher)
|
|
2651
|
+
return false;
|
|
2652
|
+
this.launcher({ appId: target.appId, route: target.route, kind, id });
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyMarkdownEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2656
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyMarkdownEditorComponent, isStandalone: true, selector: "fly-markdown-editor", inputs: { toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, enableEntityLink: { classPropertyName: "enableEntityLink", publicName: "enableEntityLink", isSignal: true, isRequired: false, transformFunction: null }, entityLinkPlacement: { classPropertyName: "entityLinkPlacement", publicName: "entityLinkPlacement", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitShortcut: "submitShortcut" }, providers: [
|
|
2657
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FlyMarkdownEditorComponent), multi: true },
|
|
2658
|
+
], ngImport: i0, template: "<div class=\"fly-md-editor\" [class.fly-md-editor--entity-above]=\"entityLinkPlacement() === 'above'\">\n <div class=\"fly-md-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"('common.label.formatting_toolbar' | translate)\">\n @for (item of toolbarItems(); track $index) {\n @switch (item) {\n @case ('|') {\n <span class=\"fly-md-editor__divider\" aria-hidden=\"true\"></span>\n }\n @case ('link') {\n <div class=\"fly-md-editor__link-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"isActive('link')\"\n (click)=\"openLinkPopover()\"\n [title]=\"'common.label.link' | translate\" [attr.aria-label]=\"'common.label.link' | translate\">\n <i class=\"pi pi-link\" aria-hidden=\"true\"></i>\n </button>\n @if (showLinkPopover()) {\n <div class=\"fly-md-editor__link-popover\" role=\"dialog\" [attr.aria-label]=\"'common.label.link' | translate\">\n <input class=\"fly-md-editor__link-input\" type=\"url\"\n [value]=\"linkInputValue()\" (input)=\"onLinkInput($any($event.target).value)\"\n (keydown)=\"onLinkKeydown($event)\"\n [placeholder]=\"'common.label.https' | translate\"\n [attr.aria-label]=\"'common.label.link' | translate\" />\n <button type=\"button\" class=\"fly-md-editor__link-ok\" (click)=\"commitLink()\"\n [title]=\"'common.label.apply' | translate\"><i class=\"pi pi-check\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" class=\"fly-md-editor__link-cancel\" (click)=\"cancelLink()\"\n [title]=\"'common.action.cancel' | translate\"><i class=\"pi pi-times\" aria-hidden=\"true\"></i></button>\n </div>\n }\n </div>\n }\n @case ('entityLink') {\n <div class=\"fly-md-editor__entity-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"showEntityLookup()\"\n (click)=\"toggleEntityLookup()\" [attr.aria-expanded]=\"showEntityLookup()\"\n [title]=\"'common.label.insert_entity_link' | translate\" [attr.aria-label]=\"'common.label.insert_entity_link' | translate\">\n <i class=\"pi pi-bookmark\" aria-hidden=\"true\"></i>\n </button>\n @if (showEntityLookup()) {\n <fly-entity-lookup\n [placement]=\"entityLinkPlacement()\"\n [descriptors]=\"entityLookupDescriptors()\"\n (entityLinkSelected)=\"onEntityLinkSelected($event)\"\n (dismiss)=\"closeEntityLookup()\" />\n }\n </div>\n }\n @default {\n <button type=\"button\" class=\"fly-md-editor__btn\"\n [class.is-active]=\"isActive(item)\" [disabled]=\"disabled(item)\"\n (click)=\"run(item)\"\n [title]=\"(button(item)?.labelKey ?? '') | translate\"\n [attr.aria-label]=\"(button(item)?.labelKey ?? '') | translate\">\n @if (textLabel(item); as txt) {\n <span class=\"fly-md-editor__btn-text\">{{ txt }}</span>\n } @else {\n <i class=\"pi {{ button(item)?.icon }}\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n }\n }\n </div>\n\n <div class=\"fly-md-editor__body\" tiptap [editor]=\"editor\"\n [attr.aria-label]=\"ariaLabel()\" [attr.data-placeholder]=\"placeholder()\"></div>\n</div>\n", styles: [":host{display:block}.fly-md-editor{display:flex;flex-direction:column;min-block-size:0;position:relative}.fly-md-editor--entity-above .fly-md-editor__entity-wrap{position:static}.fly-md-editor__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:2px;padding-block:4px}.fly-md-editor__divider{inline-size:1px;block-size:18px;margin-inline:4px;background:var(--surface-border, rgba(255, 255, 255, .12))}.fly-md-editor__btn{min-inline-size:28px;block-size:28px;padding-inline:7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-color-secondary, #6e6e73);font-size:12px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .12s ease,color .12s ease}.fly-md-editor__btn:hover:not(:disabled){background:var(--surface-hover, rgba(0, 0, 0, .05))}.fly-md-editor__btn.is-active{background:var(--primary-color-subtle, rgba(0, 113, 227, .12));color:var(--primary-color, #0071e3);border-color:var(--primary-color-border, rgba(0, 113, 227, .25))}.fly-md-editor__btn:disabled{opacity:.4;cursor:default}.fly-md-editor__btn i.pi{font-size:13px}.fly-md-editor__btn-text{font-weight:600;font-style:normal}@media(prefers-reduced-motion:reduce){.fly-md-editor__btn{transition:none}}.fly-md-editor__link-wrap,.fly-md-editor__entity-wrap{position:relative;display:inline-flex}.fly-md-editor__link-popover{position:absolute;inset-block-start:calc(100% + 4px);inset-inline-start:0;z-index:1000;display:flex;gap:4px;padding:4px;background:var(--surface-elevated, #fff);border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:6px;box-shadow:0 4px 12px #0000001f}.fly-md-editor__link-input{inline-size:200px;padding:4px 6px;font-size:12px;border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:4px;outline:none;background:transparent;color:inherit}.fly-md-editor__link-input:focus{border-color:var(--primary-color, #0071e3)}.fly-md-editor__link-ok,.fly-md-editor__link-cancel{inline-size:24px;block-size:24px;border-radius:4px;border:none;background:transparent;cursor:pointer;color:var(--text-color-secondary, #6e6e73)}.fly-md-editor__link-ok:hover,.fly-md-editor__link-cancel:hover{background:var(--surface-hover, rgba(0, 0, 0, .06))}.fly-md-editor__body{flex:1;min-block-size:56px;overflow-y:auto;padding:8px 4px;font-size:13px;line-height:1.55;color:var(--text-color, inherit);cursor:text}.fly-md-editor__body:focus-within{outline:none}::ng-deep .fly-md-editor__prosemirror{outline:none}::ng-deep .fly-md-editor__prosemirror>*{margin-block:0 .5em}::ng-deep .fly-md-editor__prosemirror>:last-child{margin-block-end:0}::ng-deep .fly-md-editor__prosemirror h1{font-size:1.4em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h2{font-size:1.2em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h3{font-size:1.05em;font-weight:700}::ng-deep .fly-md-editor__prosemirror ul,::ng-deep .fly-md-editor__prosemirror ol{padding-inline-start:1.4em}::ng-deep .fly-md-editor__prosemirror a{color:var(--primary-color, #0071e3);text-decoration:underline;cursor:pointer}::ng-deep .fly-md-editor__prosemirror code{background:var(--surface-hover, rgba(0, 0, 0, .06));padding:1px 4px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.95em}::ng-deep .fly-md-editor__prosemirror pre{background:var(--surface-hover, rgba(0, 0, 0, .06));border-radius:6px;padding:8px 10px;overflow-x:auto}::ng-deep .fly-md-editor__prosemirror pre code{background:transparent;padding:0}::ng-deep .fly-md-editor__prosemirror blockquote{margin-inline:0;padding-inline-start:10px;border-inline-start:3px solid var(--surface-border, rgba(0, 0, 0, .15));color:var(--text-color-secondary, #6e6e73)}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList]{list-style:none;padding-inline-start:.2em}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li{display:flex;align-items:flex-start;gap:6px}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li>label{margin-block-start:.15em}::ng-deep .fly-md-editor__prosemirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--text-color-secondary, rgba(0, 0, 0, .4));float:inline-start;pointer-events:none;block-size:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: TiptapEditorDirective, selector: "tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]", inputs: ["editor", "outputFormat"] }, { kind: "component", type: EntityLookupComponent, selector: "fly-entity-lookup", inputs: ["descriptors", "initialEntity", "initialQuery", "listboxId", "placement"], outputs: ["pick", "entityLinkSelected", "dismiss"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2659
|
+
}
|
|
2660
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyMarkdownEditorComponent, decorators: [{
|
|
2661
|
+
type: Component,
|
|
2662
|
+
args: [{ selector: 'fly-markdown-editor', standalone: true, imports: [CommonModule, FormsModule, TiptapEditorDirective, TranslatePipe, EntityLookupComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
2663
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FlyMarkdownEditorComponent), multi: true },
|
|
2664
|
+
], template: "<div class=\"fly-md-editor\" [class.fly-md-editor--entity-above]=\"entityLinkPlacement() === 'above'\">\n <div class=\"fly-md-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"('common.label.formatting_toolbar' | translate)\">\n @for (item of toolbarItems(); track $index) {\n @switch (item) {\n @case ('|') {\n <span class=\"fly-md-editor__divider\" aria-hidden=\"true\"></span>\n }\n @case ('link') {\n <div class=\"fly-md-editor__link-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"isActive('link')\"\n (click)=\"openLinkPopover()\"\n [title]=\"'common.label.link' | translate\" [attr.aria-label]=\"'common.label.link' | translate\">\n <i class=\"pi pi-link\" aria-hidden=\"true\"></i>\n </button>\n @if (showLinkPopover()) {\n <div class=\"fly-md-editor__link-popover\" role=\"dialog\" [attr.aria-label]=\"'common.label.link' | translate\">\n <input class=\"fly-md-editor__link-input\" type=\"url\"\n [value]=\"linkInputValue()\" (input)=\"onLinkInput($any($event.target).value)\"\n (keydown)=\"onLinkKeydown($event)\"\n [placeholder]=\"'common.label.https' | translate\"\n [attr.aria-label]=\"'common.label.link' | translate\" />\n <button type=\"button\" class=\"fly-md-editor__link-ok\" (click)=\"commitLink()\"\n [title]=\"'common.label.apply' | translate\"><i class=\"pi pi-check\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" class=\"fly-md-editor__link-cancel\" (click)=\"cancelLink()\"\n [title]=\"'common.action.cancel' | translate\"><i class=\"pi pi-times\" aria-hidden=\"true\"></i></button>\n </div>\n }\n </div>\n }\n @case ('entityLink') {\n <div class=\"fly-md-editor__entity-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"showEntityLookup()\"\n (click)=\"toggleEntityLookup()\" [attr.aria-expanded]=\"showEntityLookup()\"\n [title]=\"'common.label.insert_entity_link' | translate\" [attr.aria-label]=\"'common.label.insert_entity_link' | translate\">\n <i class=\"pi pi-bookmark\" aria-hidden=\"true\"></i>\n </button>\n @if (showEntityLookup()) {\n <fly-entity-lookup\n [placement]=\"entityLinkPlacement()\"\n [descriptors]=\"entityLookupDescriptors()\"\n (entityLinkSelected)=\"onEntityLinkSelected($event)\"\n (dismiss)=\"closeEntityLookup()\" />\n }\n </div>\n }\n @default {\n <button type=\"button\" class=\"fly-md-editor__btn\"\n [class.is-active]=\"isActive(item)\" [disabled]=\"disabled(item)\"\n (click)=\"run(item)\"\n [title]=\"(button(item)?.labelKey ?? '') | translate\"\n [attr.aria-label]=\"(button(item)?.labelKey ?? '') | translate\">\n @if (textLabel(item); as txt) {\n <span class=\"fly-md-editor__btn-text\">{{ txt }}</span>\n } @else {\n <i class=\"pi {{ button(item)?.icon }}\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n }\n }\n </div>\n\n <div class=\"fly-md-editor__body\" tiptap [editor]=\"editor\"\n [attr.aria-label]=\"ariaLabel()\" [attr.data-placeholder]=\"placeholder()\"></div>\n</div>\n", styles: [":host{display:block}.fly-md-editor{display:flex;flex-direction:column;min-block-size:0;position:relative}.fly-md-editor--entity-above .fly-md-editor__entity-wrap{position:static}.fly-md-editor__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:2px;padding-block:4px}.fly-md-editor__divider{inline-size:1px;block-size:18px;margin-inline:4px;background:var(--surface-border, rgba(255, 255, 255, .12))}.fly-md-editor__btn{min-inline-size:28px;block-size:28px;padding-inline:7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-color-secondary, #6e6e73);font-size:12px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .12s ease,color .12s ease}.fly-md-editor__btn:hover:not(:disabled){background:var(--surface-hover, rgba(0, 0, 0, .05))}.fly-md-editor__btn.is-active{background:var(--primary-color-subtle, rgba(0, 113, 227, .12));color:var(--primary-color, #0071e3);border-color:var(--primary-color-border, rgba(0, 113, 227, .25))}.fly-md-editor__btn:disabled{opacity:.4;cursor:default}.fly-md-editor__btn i.pi{font-size:13px}.fly-md-editor__btn-text{font-weight:600;font-style:normal}@media(prefers-reduced-motion:reduce){.fly-md-editor__btn{transition:none}}.fly-md-editor__link-wrap,.fly-md-editor__entity-wrap{position:relative;display:inline-flex}.fly-md-editor__link-popover{position:absolute;inset-block-start:calc(100% + 4px);inset-inline-start:0;z-index:1000;display:flex;gap:4px;padding:4px;background:var(--surface-elevated, #fff);border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:6px;box-shadow:0 4px 12px #0000001f}.fly-md-editor__link-input{inline-size:200px;padding:4px 6px;font-size:12px;border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:4px;outline:none;background:transparent;color:inherit}.fly-md-editor__link-input:focus{border-color:var(--primary-color, #0071e3)}.fly-md-editor__link-ok,.fly-md-editor__link-cancel{inline-size:24px;block-size:24px;border-radius:4px;border:none;background:transparent;cursor:pointer;color:var(--text-color-secondary, #6e6e73)}.fly-md-editor__link-ok:hover,.fly-md-editor__link-cancel:hover{background:var(--surface-hover, rgba(0, 0, 0, .06))}.fly-md-editor__body{flex:1;min-block-size:56px;overflow-y:auto;padding:8px 4px;font-size:13px;line-height:1.55;color:var(--text-color, inherit);cursor:text}.fly-md-editor__body:focus-within{outline:none}::ng-deep .fly-md-editor__prosemirror{outline:none}::ng-deep .fly-md-editor__prosemirror>*{margin-block:0 .5em}::ng-deep .fly-md-editor__prosemirror>:last-child{margin-block-end:0}::ng-deep .fly-md-editor__prosemirror h1{font-size:1.4em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h2{font-size:1.2em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h3{font-size:1.05em;font-weight:700}::ng-deep .fly-md-editor__prosemirror ul,::ng-deep .fly-md-editor__prosemirror ol{padding-inline-start:1.4em}::ng-deep .fly-md-editor__prosemirror a{color:var(--primary-color, #0071e3);text-decoration:underline;cursor:pointer}::ng-deep .fly-md-editor__prosemirror code{background:var(--surface-hover, rgba(0, 0, 0, .06));padding:1px 4px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.95em}::ng-deep .fly-md-editor__prosemirror pre{background:var(--surface-hover, rgba(0, 0, 0, .06));border-radius:6px;padding:8px 10px;overflow-x:auto}::ng-deep .fly-md-editor__prosemirror pre code{background:transparent;padding:0}::ng-deep .fly-md-editor__prosemirror blockquote{margin-inline:0;padding-inline-start:10px;border-inline-start:3px solid var(--surface-border, rgba(0, 0, 0, .15));color:var(--text-color-secondary, #6e6e73)}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList]{list-style:none;padding-inline-start:.2em}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li{display:flex;align-items:flex-start;gap:6px}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li>label{margin-block-start:.15em}::ng-deep .fly-md-editor__prosemirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--text-color-secondary, rgba(0, 0, 0, .4));float:inline-start;pointer-events:none;block-size:0}\n"] }]
|
|
2665
|
+
}], ctorParameters: () => [], propDecorators: { toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], enableEntityLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableEntityLink", required: false }] }], entityLinkPlacement: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityLinkPlacement", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], submitShortcut: [{ type: i0.Output, args: ["submitShortcut"] }] } });
|
|
2666
|
+
|
|
1631
2667
|
class ContextMenuComponent {
|
|
1632
2668
|
menuEl;
|
|
1633
2669
|
doc = inject(DOCUMENT);
|
|
@@ -1955,11 +2991,11 @@ class MessageBoxComponent {
|
|
|
1955
2991
|
this.previouslyFocused = null;
|
|
1956
2992
|
}
|
|
1957
2993
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1958
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageBoxComponent, isStandalone: true, selector: "fly-message-box", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (service.visible()) {\
|
|
2994
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageBoxComponent, isStandalone: true, selector: "fly-message-box", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (service.visible()) {\n <div\n class=\"mb-backdrop\"\n (click)=\"onBackdropClick()\"\n role=\"presentation\">\n <div\n class=\"mb-dialog\"\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\n (click)=\"$event.stopPropagation()\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"mb-title\"\n [attr.aria-describedby]=\"ariaDescribedBy()\">\n\n @if (iconClass()) {\n <div class=\"mb-icon-wrap\">\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\n </div>\n }\n\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\n\n @if (dontAskAgainConfig(); as daa) {\n <label class=\"mb-dont-ask-again\">\n <input\n type=\"checkbox\"\n [checked]=\"dontAskAgainChecked()\"\n (change)=\"onDontAskAgainToggle($event)\" />\n <span id=\"mb-dont-ask\">{{ daa.labelKey | translate }}</span>\n </label>\n }\n\n <div class=\"mb-actions\">\n @for (btn of service.buttons(); track btn.result) {\n <button\n type=\"button\"\n class=\"vos-btn sm mb-btn\"\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\n (click)=\"onButtonClick(btn.result)\">\n {{ btn.label }}\n </button>\n }\n </div>\n\n </div>\n </div>\n}\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-dont-ask-again{display:flex;align-items:center;gap:8px;margin-block-start:12px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;align-self:flex-start;-webkit-user-select:none;user-select:none}.mb-dont-ask-again input[type=checkbox]{margin:0;cursor:pointer}.mb-dont-ask-again span{line-height:1.3}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1959
2995
|
}
|
|
1960
2996
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageBoxComponent, decorators: [{
|
|
1961
2997
|
type: Component,
|
|
1962
|
-
args: [{ selector: 'fly-message-box', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (service.visible()) {\
|
|
2998
|
+
args: [{ selector: 'fly-message-box', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (service.visible()) {\n <div\n class=\"mb-backdrop\"\n (click)=\"onBackdropClick()\"\n role=\"presentation\">\n <div\n class=\"mb-dialog\"\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\n (click)=\"$event.stopPropagation()\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"mb-title\"\n [attr.aria-describedby]=\"ariaDescribedBy()\">\n\n @if (iconClass()) {\n <div class=\"mb-icon-wrap\">\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\n </div>\n }\n\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\n\n @if (dontAskAgainConfig(); as daa) {\n <label class=\"mb-dont-ask-again\">\n <input\n type=\"checkbox\"\n [checked]=\"dontAskAgainChecked()\"\n (change)=\"onDontAskAgainToggle($event)\" />\n <span id=\"mb-dont-ask\">{{ daa.labelKey | translate }}</span>\n </label>\n }\n\n <div class=\"mb-actions\">\n @for (btn of service.buttons(); track btn.result) {\n <button\n type=\"button\"\n class=\"vos-btn sm mb-btn\"\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\n (click)=\"onButtonClick(btn.result)\">\n {{ btn.label }}\n </button>\n }\n </div>\n\n </div>\n </div>\n}\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-dont-ask-again{display:flex;align-items:center;gap:8px;margin-block-start:12px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;align-self:flex-start;-webkit-user-select:none;user-select:none}.mb-dont-ask-again input[type=checkbox]{margin:0;cursor:pointer}.mb-dont-ask-again span{line-height:1.3}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"] }]
|
|
1963
2999
|
}], propDecorators: { onEscape: [{
|
|
1964
3000
|
type: HostListener,
|
|
1965
3001
|
args: ['document:keydown.escape']
|
|
@@ -2291,11 +3327,11 @@ class SharePanelComponent {
|
|
|
2291
3327
|
}
|
|
2292
3328
|
}
|
|
2293
3329
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2294
|
-
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", defaultChartSystemKey: "defaultChartSystemKey", defaultChartId: "defaultChartId", loadOuLabelMap: "loadOuLabelMap", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", supportsDeny: "supportsDeny", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey", loadRoleOus: "loadRoleOus" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\r\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"fac-header\">\r\n <h3>{{ titleKey | translate }}</h3>\r\n <span class=\"fac-target-name\">{{ targetName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fac-close\"\r\n (click)=\"close.emit()\"\r\n [attr.aria-label]=\"'files.share.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <div class=\"fac-add-section\">\r\n <div class=\"fac-search-row\">\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"searchQuery\"\r\n (input)=\"onSearchInput()\"\r\n [placeholder]=\"'files.share.search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_users' | translate\"\r\n />\r\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (searchResults().length > 0) {\r\n <div class=\"fac-search-results\">\r\n @for (result of searchResults(); track result.id) {\r\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ result.firstName }} {{ result.lastName }}</span>\r\n <span class=\"fac-email\">{{ result.email }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (chartOptions().length > 0) {\r\n <div class=\"fac-chart-row\">\r\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\r\n 'files.share.org_chart' | translate\r\n }}</label>\r\n <select\r\n id=\"fac-org-chart-select\"\r\n class=\"fac-select\"\r\n [ngModel]=\"selectedOrgChartId()\"\r\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n @if (showOuPicker()) {\r\n <div class=\"fac-ou-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-ou-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"onGrantToOu(ou)\"\r\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <div class=\"fac-role-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"roleSearchQuery\"\r\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\r\n [hidden]=\"!showRolePicker()\" />\r\n @if (showRolePicker()) {\r\n <div class=\"fac-role-list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-role-item\"\r\n (click)=\"onGrantToRole(role)\"\r\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ role.displayName }}</span>\r\n <span class=\"fac-role-app\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <div class=\"fac-toggle-row\">\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\r\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\r\n </button>\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\r\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\r\n </button>\r\n }\r\n @if (showApplyToChildren) {\r\n <label class=\"fac-checkbox-label\">\r\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\r\n {{ 'files.share.apply_children' | translate }}\r\n </label>\r\n }\r\n @if (supportsDeny) {\r\n <label class=\"fac-checkbox-label fac-deny-toggle\"\r\n [title]=\"'files.share.deny_help' | translate\">\r\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\r\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\r\n {{ 'files.share.add_as_deny' | translate }}\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fac-perms-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\r\n @if (loading()) {\r\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\r\n } @else if (permissions().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\r\n } @else {\r\n @for (perm of permissions(); track perm.id) {\r\n <div class=\"fac-perm-row\" [class.fac-perm-row-deny]=\"perm.isDeny\">\r\n <div class=\"fac-perm-info\">\r\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\r\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\r\n @if (perm.isDeny) {\r\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\r\n }\r\n @if (perm.isInherited) {\r\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\r\n }\r\n </div>\r\n <div class=\"fac-perm-actions\">\r\n @if (!perm.isDeny) {\r\n <select\r\n class=\"fac-select sm\"\r\n [ngModel]=\"perm.level\"\r\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n >\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"fac-icon-btn danger\"\r\n (click)=\"onRevokePermission(perm)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n [title]=\"'files.share.revoke' | translate\"\r\n >\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n</div>\r\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-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.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 });
|
|
3330
|
+
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", defaultChartSystemKey: "defaultChartSystemKey", defaultChartId: "defaultChartId", loadOuLabelMap: "loadOuLabelMap", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", supportsDeny: "supportsDeny", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey", loadRoleOus: "loadRoleOus" }, 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\n type=\"button\"\n class=\"fac-close\"\n (click)=\"close.emit()\"\n [attr.aria-label]=\"'files.share.close' | translate\">\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 [attr.aria-label]=\"'files.share.search_users' | 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 <button\n type=\"button\"\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span dir=\"auto\">{{ ou.displayName }}</span>\n </button>\n }\n </div>\n }\n\n @if (loadRoleOus && roleOus().length > 0) {\n <div class=\"fac-role-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"roleSearchQuery\"\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\n [hidden]=\"!showRolePicker()\" />\n @if (showRolePicker()) {\n <div class=\"fac-role-list\">\n @for (role of filteredRoles(); track role.ouId) {\n <button\n type=\"button\"\n class=\"fac-role-item\"\n (click)=\"onGrantToRole(role)\"\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span dir=\"auto\">{{ role.displayName }}</span>\n <span class=\"fac-role-app\">{{ role.appId }}</span>\n </button>\n }\n @if (filteredRoles().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\n }\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 (loadRoleOus && roleOus().length > 0) {\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\n </button>\n }\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 @if (supportsDeny) {\n <label class=\"fac-checkbox-label fac-deny-toggle\"\n [title]=\"'files.share.deny_help' | translate\">\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\n {{ 'files.share.add_as_deny' | 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\" [class.fac-perm-row-deny]=\"perm.isDeny\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\n @if (perm.isDeny) {\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\n }\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 @if (!perm.isDeny) {\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 }\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-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.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 });
|
|
2295
3331
|
}
|
|
2296
3332
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, decorators: [{
|
|
2297
3333
|
type: Component,
|
|
2298
|
-
args: [{ selector: 'fly-share-panel', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\r\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"fac-header\">\r\n <h3>{{ titleKey | translate }}</h3>\r\n <span class=\"fac-target-name\">{{ targetName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fac-close\"\r\n (click)=\"close.emit()\"\r\n [attr.aria-label]=\"'files.share.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <div class=\"fac-add-section\">\r\n <div class=\"fac-search-row\">\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"searchQuery\"\r\n (input)=\"onSearchInput()\"\r\n [placeholder]=\"'files.share.search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_users' | translate\"\r\n />\r\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (searchResults().length > 0) {\r\n <div class=\"fac-search-results\">\r\n @for (result of searchResults(); track result.id) {\r\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ result.firstName }} {{ result.lastName }}</span>\r\n <span class=\"fac-email\">{{ result.email }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (chartOptions().length > 0) {\r\n <div class=\"fac-chart-row\">\r\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\r\n 'files.share.org_chart' | translate\r\n }}</label>\r\n <select\r\n id=\"fac-org-chart-select\"\r\n class=\"fac-select\"\r\n [ngModel]=\"selectedOrgChartId()\"\r\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n @if (showOuPicker()) {\r\n <div class=\"fac-ou-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-ou-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"onGrantToOu(ou)\"\r\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <div class=\"fac-role-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"roleSearchQuery\"\r\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\r\n [hidden]=\"!showRolePicker()\" />\r\n @if (showRolePicker()) {\r\n <div class=\"fac-role-list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-role-item\"\r\n (click)=\"onGrantToRole(role)\"\r\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ role.displayName }}</span>\r\n <span class=\"fac-role-app\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <div class=\"fac-toggle-row\">\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\r\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\r\n </button>\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\r\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\r\n </button>\r\n }\r\n @if (showApplyToChildren) {\r\n <label class=\"fac-checkbox-label\">\r\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\r\n {{ 'files.share.apply_children' | translate }}\r\n </label>\r\n }\r\n @if (supportsDeny) {\r\n <label class=\"fac-checkbox-label fac-deny-toggle\"\r\n [title]=\"'files.share.deny_help' | translate\">\r\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\r\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\r\n {{ 'files.share.add_as_deny' | translate }}\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fac-perms-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\r\n @if (loading()) {\r\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\r\n } @else if (permissions().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\r\n } @else {\r\n @for (perm of permissions(); track perm.id) {\r\n <div class=\"fac-perm-row\" [class.fac-perm-row-deny]=\"perm.isDeny\">\r\n <div class=\"fac-perm-info\">\r\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\r\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\r\n @if (perm.isDeny) {\r\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\r\n }\r\n @if (perm.isInherited) {\r\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\r\n }\r\n </div>\r\n <div class=\"fac-perm-actions\">\r\n @if (!perm.isDeny) {\r\n <select\r\n class=\"fac-select sm\"\r\n [ngModel]=\"perm.level\"\r\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n >\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"fac-icon-btn danger\"\r\n (click)=\"onRevokePermission(perm)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n [title]=\"'files.share.revoke' | translate\"\r\n >\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n</div>\r\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-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.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"] }]
|
|
3334
|
+
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\n type=\"button\"\n class=\"fac-close\"\n (click)=\"close.emit()\"\n [attr.aria-label]=\"'files.share.close' | translate\">\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 [attr.aria-label]=\"'files.share.search_users' | 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 <button\n type=\"button\"\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span dir=\"auto\">{{ ou.displayName }}</span>\n </button>\n }\n </div>\n }\n\n @if (loadRoleOus && roleOus().length > 0) {\n <div class=\"fac-role-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"roleSearchQuery\"\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\n [hidden]=\"!showRolePicker()\" />\n @if (showRolePicker()) {\n <div class=\"fac-role-list\">\n @for (role of filteredRoles(); track role.ouId) {\n <button\n type=\"button\"\n class=\"fac-role-item\"\n (click)=\"onGrantToRole(role)\"\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span dir=\"auto\">{{ role.displayName }}</span>\n <span class=\"fac-role-app\">{{ role.appId }}</span>\n </button>\n }\n @if (filteredRoles().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\n }\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 (loadRoleOus && roleOus().length > 0) {\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\n </button>\n }\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 @if (supportsDeny) {\n <label class=\"fac-checkbox-label fac-deny-toggle\"\n [title]=\"'files.share.deny_help' | translate\">\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\n {{ 'files.share.add_as_deny' | 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\" [class.fac-perm-row-deny]=\"perm.isDeny\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\n @if (perm.isDeny) {\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\n }\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 @if (!perm.isDeny) {\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 }\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-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.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"] }]
|
|
2299
3335
|
}], propDecorators: { targetName: [{
|
|
2300
3336
|
type: Input
|
|
2301
3337
|
}], titleKey: [{
|
|
@@ -2941,7 +3977,7 @@ class AudienceBuilderComponent {
|
|
|
2941
3977
|
useExisting: forwardRef(() => AudienceBuilderComponent),
|
|
2942
3978
|
multi: true,
|
|
2943
3979
|
},
|
|
2944
|
-
], usesOnChanges: true, ngImport: i0, template: "<div class=\"fab-root\">\r\n\r\n <!-- Includes section -->\r\n <div class=\"fab-bucket\">\r\n <div class=\"fab-bucket-header\">\r\n <div class=\"fab-section-label\" id=\"fab-includes-label\">{{ 'audience.includes' | translate }}</div>\r\n <button\r\n type=\"button\"\r\n class=\"fab-add-btn\"\r\n (click)=\"openPicker('includes')\"\r\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'includes'\"\r\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'includes' ? 'fab-picker-region' : null\"\r\n >\r\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\r\n {{ 'audience.add_term' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (includes().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.includes_empty' | translate }}</div>\r\n } @else {\r\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-includes-label\">\r\n @for (term of includes(); track term) {\r\n <li class=\"fab-term\">\r\n <ng-container\r\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'includes', index: $index }\">\r\n </ng-container>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n\r\n <!-- Excludes section -->\r\n @if (showExcludes) {\r\n <div class=\"fab-bucket fab-bucket-exclude\">\r\n <div class=\"fab-bucket-header\">\r\n <div class=\"fab-section-label\" id=\"fab-excludes-label\">{{ 'audience.excludes' | translate }}</div>\r\n <button\r\n type=\"button\"\r\n class=\"fab-add-btn\"\r\n (click)=\"openPicker('excludes')\"\r\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'excludes'\"\r\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'excludes' ? 'fab-picker-region' : null\"\r\n >\r\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\r\n {{ 'audience.add_term' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (excludes().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.excludes_empty' | translate }}</div>\r\n } @else {\r\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-excludes-label\">\r\n @for (term of excludes(); track term) {\r\n <li class=\"fab-term\">\r\n <ng-container\r\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'excludes', index: $index }\">\r\n </ng-container>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Inline picker (shared across includes/excludes; gated by pickerOpen) -->\r\n @if (pickerOpen()) {\r\n <div\r\n id=\"fab-picker-region\"\r\n class=\"fab-picker\"\r\n role=\"region\"\r\n [attr.aria-label]=\"(editTarget() === 'includes'\r\n ? ('audience.adding_to_includes' | translate)\r\n : ('audience.adding_to_excludes' | translate))\"\r\n >\r\n <div class=\"fab-picker-header\">\r\n <span class=\"fab-picker-target\">\r\n {{\r\n editTarget() === 'includes'\r\n ? ('audience.adding_to_includes' | translate)\r\n : ('audience.adding_to_excludes' | translate)\r\n }}\r\n </span>\r\n <button type=\"button\" class=\"fab-close\" (click)=\"closePicker()\" [attr.aria-label]=\"'audience.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <!-- Kind tabs -->\r\n <div class=\"fab-kind-tabs\" role=\"tablist\">\r\n @for (kind of supportedKinds; track kind) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n class=\"fab-kind-tab\"\r\n [id]=\"'fab-tab-' + kind\"\r\n [class.active]=\"activeKind() === kind\"\r\n [attr.aria-selected]=\"activeKind() === kind\"\r\n [attr.aria-controls]=\"'fab-tabpanel-' + kind\"\r\n (click)=\"selectKind(kind)\"\r\n >\r\n <i [class]=\"kindIconClass(kind)\" aria-hidden=\"true\"></i>\r\n <span>{{ kindLocaleKey(kind) | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (errorKey()) {\r\n <div class=\"fab-inline-error\" role=\"alert\">\r\n <i class=\"pi pi-exclamation-triangle\" aria-hidden=\"true\"></i>\r\n {{ errorKey()! | translate }}\r\n </div>\r\n }\r\n\r\n <!-- Per-kind picker bodies -->\r\n <div\r\n class=\"fab-picker-body\"\r\n role=\"tabpanel\"\r\n [id]=\"'fab-tabpanel-' + activeKind()\"\r\n [attr.aria-labelledby]=\"'fab-tab-' + activeKind()\"\r\n >\r\n @switch (activeKind()) {\r\n\r\n @case ('users') {\r\n <input\r\n type=\"text\"\r\n class=\"fab-input\"\r\n [ngModel]=\"userSearchQuery()\"\r\n (ngModelChange)=\"userSearchQuery.set($event); onUserSearchInput()\"\r\n [placeholder]=\"'audience.search_users_placeholder' | translate\"\r\n [attr.aria-label]=\"'audience.search_users_placeholder' | translate\"\r\n />\r\n @if (userSearchResults().length > 0) {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (u of userSearchResults(); track u.id) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickUser(u)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ u.firstName }} {{ u.lastName }}</span>\r\n <span class=\"fab-result-secondary\">{{ u.email }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else if (userSearchQuery().length >= 2) {\r\n <div class=\"fab-empty\">{{ 'audience.no_users_match' | translate }}</div>\r\n }\r\n }\r\n\r\n @case ('roles') {\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <input\r\n type=\"text\"\r\n class=\"fab-input\"\r\n [ngModel]=\"roleSearchQuery()\"\r\n (ngModelChange)=\"roleSearchQuery.set($event)\"\r\n [placeholder]=\"'audience.search_roles_placeholder' | translate\"\r\n [attr.aria-label]=\"'audience.search_roles_placeholder' | translate\"\r\n />\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickRole(role)\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span>{{ role.displayName }}</span>\r\n <span class=\"fab-result-secondary\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"fab-empty\">{{ 'audience.roles_unavailable' | translate }}</div>\r\n }\r\n }\r\n\r\n @case ('ou') {\r\n @if (chartOptions().length > 0) {\r\n <label for=\"fab-chart-select\" class=\"fab-mini-label\">\r\n {{ 'audience.org_chart' | translate }}\r\n </label>\r\n <select\r\n id=\"fab-chart-select\"\r\n class=\"fab-select\"\r\n [ngModel]=\"selectedChartId()\"\r\n (ngModelChange)=\"onChartChange($event)\"\r\n >\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n }\r\n <label class=\"fab-checkbox-label\">\r\n <input\r\n type=\"checkbox\"\r\n [ngModel]=\"ouIncludeDescendants()\"\r\n (ngModelChange)=\"ouIncludeDescendants.set($event)\"\r\n />\r\n {{ 'audience.include_descendants' | translate }}\r\n </label>\r\n @if (ouTree().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.ou_tree_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list fab-ou-tree\" role=\"list\">\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n role=\"listitem\"\r\n class=\"fab-result-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"pickOu(ou)\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span>{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('app-everyone') {\r\n @if (appEveryoneOptions.length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.app_everyone_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (app of appEveryoneOptions; track app.appId) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickAppEveryone(app.appId)\">\r\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\r\n <span>{{ app.displayName }}</span>\r\n <span class=\"fab-result-secondary\">{{ app.appId }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('chart') {\r\n @if (chartTermOptions().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.chart_options_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (opt of chartTermOptions(); track opt.id) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickChart(opt.id)\">\r\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\r\n <span>{{ opt.name }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('preset') {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (p of presetOptions; track p) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickPreset(p)\">\r\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\r\n <span>{{ presetLocaleKey(p) | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n</div>\r\n\r\n<!-- Reusable term card template, parameterised by target bucket + index for removal callbacks -->\r\n<ng-template #termCard let-term let-target=\"target\" let-index=\"index\">\r\n <div class=\"fab-term-card\" [class.fab-term-exclude]=\"target === 'excludes'\">\r\n\r\n @switch (term.kind) {\r\n @case ('users') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_users' | translate: { count: term.userIds.length } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (id of term.userIds; track id) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ id.substring(0, 8) }}\u2026</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeUserId(target, index, id)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('roles') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_roles' | translate: { app: appLabel(term.appId), count: term.roleKeys.length } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (k of term.roleKeys; track k) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span>{{ roleLabel(term.appId, k) }}</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeRoleKey(target, index, k)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('ou') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_ous' | translate: { count: term.ouIds.length } }}\r\n @if (term.includeDescendants) {\r\n <span class=\"fab-badge\">{{ 'audience.descendants_badge' | translate }}</span>\r\n }\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (id of term.ouIds; track id) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span>{{ ouLabel(id) }}</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeOuId(target, index, id)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('app-everyone') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_app_everyone' | translate: { app: appLabel(term.appId) } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n\r\n @case ('chart') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_chart' | translate: { chart: chartLabel(term.chartId) } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n\r\n @case ('preset') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ presetLocaleKey(term.preset) | translate }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: [".fab-root{display:flex;flex-direction:column;gap:14px;width:100%}.fab-bucket{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px}.fab-bucket.fab-bucket-exclude{border-inline-start:3px solid rgba(255,59,48,.55)}.fab-bucket-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fab-section-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-color-secondary)}.fab-add-btn{display:inline-flex;align-items:center;gap:6px;background:var(--primary-color, #e8732a);color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer}.fab-add-btn:hover{filter:brightness(1.05)}.fab-add-btn i{font-size:11px}.fab-empty{text-align:center;padding:12px;color:var(--text-color-secondary);font-size:12px;font-style:italic}.fab-term-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.fab-term-card{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px}.fab-term-card.fab-term-exclude{background:#ff3b300d;border-color:#ff3b302e}.fab-term-head{display:flex;align-items:center;gap:8px}.fab-term-head i:first-child{color:var(--text-color-secondary)}.fab-term-title{flex:1;font-size:13px;display:inline-flex;align-items:center;gap:6px}.fab-badge{font-size:10px;background:#ffffff1a;color:var(--text-color-secondary);padding:2px 6px;border-radius:8px;text-transform:uppercase;letter-spacing:.5px}.fab-term-chips{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}.fab-chip{display:inline-flex;align-items:center;gap:4px;background:#ffffff0f;border-radius:12px;padding:2px 4px 2px 8px;font-size:11px}.fab-chip i{font-size:10px;color:var(--text-color-secondary)}.fab-chip-x{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;padding:2px 4px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center}.fab-chip-x i{font-size:9px}.fab-chip-x:hover{background:#ff3b302e;color:#ff3b30}.fab-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:11px}.fab-icon-btn:hover{background:#ffffff24}.fab-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fab-picker{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 6px 18px #0000002e}.fab-picker-header{display:flex;align-items:center;justify-content:space-between}.fab-picker-target{font-size:12px;color:var(--text-color-secondary)}.fab-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:14px;padding:4px}.fab-close:hover{color:var(--text-color)}.fab-kind-tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--surface-border);padding-bottom:6px}.fab-kind-tab{background:transparent;border:1px solid transparent;border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.fab-kind-tab i{font-size:11px}.fab-kind-tab:hover{background:var(--surface-hover);color:var(--text-color)}.fab-kind-tab.active{background:#e8732a1f;border-color:var(--primary-color, #e8732a);color:var(--text-color)}.fab-picker-body{display:flex;flex-direction:column;gap:8px}.fab-input,.fab-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fab-input:focus,.fab-select:focus{border-color:var(--primary-color, #e8732a)}.fab-mini-label{font-size:11px;color:var(--text-color-secondary)}.fab-result-list{max-height:200px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fab-result-list.fab-ou-tree{max-height:240px}button.fab-result-item{width:100%;background:transparent;border:none;color:inherit;font:inherit;text-align:start;appearance:none}.fab-result-item{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:13px}.fab-result-item:hover{background:var(--surface-hover)}.fab-result-item:focus-visible{outline:2px solid var(--primary-color, #e8732a);outline-offset:-2px}.fab-result-item i{color:var(--text-color-secondary)}.fab-result-secondary{color:var(--text-color-secondary);font-size:11px;margin-inline-start:auto;text-transform:uppercase;letter-spacing:.4px}.fab-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fab-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fab-inline-error{display:flex;align-items:center;gap:6px;background:#ff3b301a;color:#ff8a80;border-radius:6px;padding:6px 10px;font-size:12px}.fab-inline-error i{font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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 });
|
|
3980
|
+
], usesOnChanges: true, ngImport: i0, template: "<div class=\"fab-root\">\n\n <!-- Includes section -->\n <div class=\"fab-bucket\">\n <div class=\"fab-bucket-header\">\n <div class=\"fab-section-label\" id=\"fab-includes-label\">{{ 'audience.includes' | translate }}</div>\n <button\n type=\"button\"\n class=\"fab-add-btn\"\n (click)=\"openPicker('includes')\"\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'includes'\"\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'includes' ? 'fab-picker-region' : null\"\n >\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\n {{ 'audience.add_term' | translate }}\n </button>\n </div>\n\n @if (includes().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.includes_empty' | translate }}</div>\n } @else {\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-includes-label\">\n @for (term of includes(); track term) {\n <li class=\"fab-term\">\n <ng-container\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'includes', index: $index }\">\n </ng-container>\n </li>\n }\n </ul>\n }\n </div>\n\n <!-- Excludes section -->\n @if (showExcludes) {\n <div class=\"fab-bucket fab-bucket-exclude\">\n <div class=\"fab-bucket-header\">\n <div class=\"fab-section-label\" id=\"fab-excludes-label\">{{ 'audience.excludes' | translate }}</div>\n <button\n type=\"button\"\n class=\"fab-add-btn\"\n (click)=\"openPicker('excludes')\"\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'excludes'\"\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'excludes' ? 'fab-picker-region' : null\"\n >\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\n {{ 'audience.add_term' | translate }}\n </button>\n </div>\n\n @if (excludes().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.excludes_empty' | translate }}</div>\n } @else {\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-excludes-label\">\n @for (term of excludes(); track term) {\n <li class=\"fab-term\">\n <ng-container\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'excludes', index: $index }\">\n </ng-container>\n </li>\n }\n </ul>\n }\n </div>\n }\n\n <!-- Inline picker (shared across includes/excludes; gated by pickerOpen) -->\n @if (pickerOpen()) {\n <div\n id=\"fab-picker-region\"\n class=\"fab-picker\"\n role=\"region\"\n [attr.aria-label]=\"(editTarget() === 'includes'\n ? ('audience.adding_to_includes' | translate)\n : ('audience.adding_to_excludes' | translate))\"\n >\n <div class=\"fab-picker-header\">\n <span class=\"fab-picker-target\">\n {{\n editTarget() === 'includes'\n ? ('audience.adding_to_includes' | translate)\n : ('audience.adding_to_excludes' | translate)\n }}\n </span>\n <button type=\"button\" class=\"fab-close\" (click)=\"closePicker()\" [attr.aria-label]=\"'audience.close' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <!-- Kind tabs -->\n <div class=\"fab-kind-tabs\" role=\"tablist\">\n @for (kind of supportedKinds; track kind) {\n <button\n type=\"button\"\n role=\"tab\"\n class=\"fab-kind-tab\"\n [id]=\"'fab-tab-' + kind\"\n [class.active]=\"activeKind() === kind\"\n [attr.aria-selected]=\"activeKind() === kind\"\n [attr.aria-controls]=\"'fab-tabpanel-' + kind\"\n (click)=\"selectKind(kind)\"\n >\n <i [class]=\"kindIconClass(kind)\" aria-hidden=\"true\"></i>\n <span>{{ kindLocaleKey(kind) | translate }}</span>\n </button>\n }\n </div>\n\n @if (errorKey()) {\n <div class=\"fab-inline-error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle\" aria-hidden=\"true\"></i>\n {{ errorKey()! | translate }}\n </div>\n }\n\n <!-- Per-kind picker bodies -->\n <div\n class=\"fab-picker-body\"\n role=\"tabpanel\"\n [id]=\"'fab-tabpanel-' + activeKind()\"\n [attr.aria-labelledby]=\"'fab-tab-' + activeKind()\"\n >\n @switch (activeKind()) {\n\n @case ('users') {\n <input\n type=\"text\"\n class=\"fab-input\"\n [ngModel]=\"userSearchQuery()\"\n (ngModelChange)=\"userSearchQuery.set($event); onUserSearchInput()\"\n [placeholder]=\"'audience.search_users_placeholder' | translate\"\n [attr.aria-label]=\"'audience.search_users_placeholder' | translate\"\n />\n @if (userSearchResults().length > 0) {\n <div class=\"fab-result-list\" role=\"list\">\n @for (u of userSearchResults(); track u.id) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickUser(u)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ u.firstName }} {{ u.lastName }}</span>\n <span class=\"fab-result-secondary\">{{ u.email }}</span>\n </button>\n }\n </div>\n } @else if (userSearchQuery().length >= 2) {\n <div class=\"fab-empty\">{{ 'audience.no_users_match' | translate }}</div>\n }\n }\n\n @case ('roles') {\n @if (loadRoleOus && roleOus().length > 0) {\n <input\n type=\"text\"\n class=\"fab-input\"\n [ngModel]=\"roleSearchQuery()\"\n (ngModelChange)=\"roleSearchQuery.set($event)\"\n [placeholder]=\"'audience.search_roles_placeholder' | translate\"\n [attr.aria-label]=\"'audience.search_roles_placeholder' | translate\"\n />\n <div class=\"fab-result-list\" role=\"list\">\n @for (role of filteredRoles(); track role.ouId) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickRole(role)\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span>{{ role.displayName }}</span>\n <span class=\"fab-result-secondary\">{{ role.appId }}</span>\n </button>\n }\n @if (filteredRoles().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.no_roles_match' | translate }}</div>\n }\n </div>\n } @else {\n <div class=\"fab-empty\">{{ 'audience.roles_unavailable' | translate }}</div>\n }\n }\n\n @case ('ou') {\n @if (chartOptions().length > 0) {\n <label for=\"fab-chart-select\" class=\"fab-mini-label\">\n {{ 'audience.org_chart' | translate }}\n </label>\n <select\n id=\"fab-chart-select\"\n class=\"fab-select\"\n [ngModel]=\"selectedChartId()\"\n (ngModelChange)=\"onChartChange($event)\"\n >\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n }\n <label class=\"fab-checkbox-label\">\n <input\n type=\"checkbox\"\n [ngModel]=\"ouIncludeDescendants()\"\n (ngModelChange)=\"ouIncludeDescendants.set($event)\"\n />\n {{ 'audience.include_descendants' | translate }}\n </label>\n @if (ouTree().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.ou_tree_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list fab-ou-tree\" role=\"list\">\n @for (ou of ouTree(); track ou.id) {\n <button\n type=\"button\"\n role=\"listitem\"\n class=\"fab-result-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"pickOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('app-everyone') {\n @if (appEveryoneOptions.length === 0) {\n <div class=\"fab-empty\">{{ 'audience.app_everyone_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list\" role=\"list\">\n @for (app of appEveryoneOptions; track app.appId) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickAppEveryone(app.appId)\">\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\n <span>{{ app.displayName }}</span>\n <span class=\"fab-result-secondary\">{{ app.appId }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('chart') {\n @if (chartTermOptions().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.chart_options_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list\" role=\"list\">\n @for (opt of chartTermOptions(); track opt.id) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickChart(opt.id)\">\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\n <span>{{ opt.name }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('preset') {\n <div class=\"fab-result-list\" role=\"list\">\n @for (p of presetOptions; track p) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickPreset(p)\">\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\n <span>{{ presetLocaleKey(p) | translate }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n </div>\n }\n\n</div>\n\n<!-- Reusable term card template, parameterised by target bucket + index for removal callbacks -->\n<ng-template #termCard let-term let-target=\"target\" let-index=\"index\">\n <div class=\"fab-term-card\" [class.fab-term-exclude]=\"target === 'excludes'\">\n\n @switch (term.kind) {\n @case ('users') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_users' | translate: { count: term.userIds.length } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (id of term.userIds; track id) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ id.substring(0, 8) }}\u2026</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeUserId(target, index, id)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('roles') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_roles' | translate: { app: appLabel(term.appId), count: term.roleKeys.length } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (k of term.roleKeys; track k) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span>{{ roleLabel(term.appId, k) }}</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeRoleKey(target, index, k)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('ou') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_ous' | translate: { count: term.ouIds.length } }}\n @if (term.includeDescendants) {\n <span class=\"fab-badge\">{{ 'audience.descendants_badge' | translate }}</span>\n }\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (id of term.ouIds; track id) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ouLabel(id) }}</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeOuId(target, index, id)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('app-everyone') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_app_everyone' | translate: { app: appLabel(term.appId) } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n\n @case ('chart') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_chart' | translate: { chart: chartLabel(term.chartId) } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n\n @case ('preset') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ presetLocaleKey(term.preset) | translate }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n }\n </div>\n</ng-template>\n", styles: [".fab-root{display:flex;flex-direction:column;gap:14px;width:100%}.fab-bucket{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px}.fab-bucket.fab-bucket-exclude{border-inline-start:3px solid rgba(255,59,48,.55)}.fab-bucket-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fab-section-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-color-secondary)}.fab-add-btn{display:inline-flex;align-items:center;gap:6px;background:var(--primary-color, #e8732a);color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer}.fab-add-btn:hover{filter:brightness(1.05)}.fab-add-btn i{font-size:11px}.fab-empty{text-align:center;padding:12px;color:var(--text-color-secondary);font-size:12px;font-style:italic}.fab-term-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.fab-term-card{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px}.fab-term-card.fab-term-exclude{background:#ff3b300d;border-color:#ff3b302e}.fab-term-head{display:flex;align-items:center;gap:8px}.fab-term-head i:first-child{color:var(--text-color-secondary)}.fab-term-title{flex:1;font-size:13px;display:inline-flex;align-items:center;gap:6px}.fab-badge{font-size:10px;background:#ffffff1a;color:var(--text-color-secondary);padding:2px 6px;border-radius:8px;text-transform:uppercase;letter-spacing:.5px}.fab-term-chips{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}.fab-chip{display:inline-flex;align-items:center;gap:4px;background:#ffffff0f;border-radius:12px;padding:2px 4px 2px 8px;font-size:11px}.fab-chip i{font-size:10px;color:var(--text-color-secondary)}.fab-chip-x{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;padding:2px 4px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center}.fab-chip-x i{font-size:9px}.fab-chip-x:hover{background:#ff3b302e;color:#ff3b30}.fab-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:11px}.fab-icon-btn:hover{background:#ffffff24}.fab-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fab-picker{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 6px 18px #0000002e}.fab-picker-header{display:flex;align-items:center;justify-content:space-between}.fab-picker-target{font-size:12px;color:var(--text-color-secondary)}.fab-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:14px;padding:4px}.fab-close:hover{color:var(--text-color)}.fab-kind-tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--surface-border);padding-bottom:6px}.fab-kind-tab{background:transparent;border:1px solid transparent;border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.fab-kind-tab i{font-size:11px}.fab-kind-tab:hover{background:var(--surface-hover);color:var(--text-color)}.fab-kind-tab.active{background:#e8732a1f;border-color:var(--primary-color, #e8732a);color:var(--text-color)}.fab-picker-body{display:flex;flex-direction:column;gap:8px}.fab-input,.fab-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fab-input:focus,.fab-select:focus{border-color:var(--primary-color, #e8732a)}.fab-mini-label{font-size:11px;color:var(--text-color-secondary)}.fab-result-list{max-height:200px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fab-result-list.fab-ou-tree{max-height:240px}button.fab-result-item{width:100%;background:transparent;border:none;color:inherit;font:inherit;text-align:start;appearance:none}.fab-result-item{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:13px}.fab-result-item:hover{background:var(--surface-hover)}.fab-result-item:focus-visible{outline:2px solid var(--primary-color, #e8732a);outline-offset:-2px}.fab-result-item i{color:var(--text-color-secondary)}.fab-result-secondary{color:var(--text-color-secondary);font-size:11px;margin-inline-start:auto;text-transform:uppercase;letter-spacing:.4px}.fab-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fab-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fab-inline-error{display:flex;align-items:center;gap:6px;background:#ff3b301a;color:#ff8a80;border-radius:6px;padding:6px 10px;font-size:12px}.fab-inline-error i{font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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 });
|
|
2945
3981
|
}
|
|
2946
3982
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudienceBuilderComponent, decorators: [{
|
|
2947
3983
|
type: Component,
|
|
@@ -2956,7 +3992,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
2956
3992
|
useExisting: forwardRef(() => AudienceBuilderComponent),
|
|
2957
3993
|
multi: true,
|
|
2958
3994
|
},
|
|
2959
|
-
], template: "<div class=\"fab-root\">\r\n\r\n <!-- Includes section -->\r\n <div class=\"fab-bucket\">\r\n <div class=\"fab-bucket-header\">\r\n <div class=\"fab-section-label\" id=\"fab-includes-label\">{{ 'audience.includes' | translate }}</div>\r\n <button\r\n type=\"button\"\r\n class=\"fab-add-btn\"\r\n (click)=\"openPicker('includes')\"\r\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'includes'\"\r\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'includes' ? 'fab-picker-region' : null\"\r\n >\r\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\r\n {{ 'audience.add_term' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (includes().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.includes_empty' | translate }}</div>\r\n } @else {\r\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-includes-label\">\r\n @for (term of includes(); track term) {\r\n <li class=\"fab-term\">\r\n <ng-container\r\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'includes', index: $index }\">\r\n </ng-container>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n\r\n <!-- Excludes section -->\r\n @if (showExcludes) {\r\n <div class=\"fab-bucket fab-bucket-exclude\">\r\n <div class=\"fab-bucket-header\">\r\n <div class=\"fab-section-label\" id=\"fab-excludes-label\">{{ 'audience.excludes' | translate }}</div>\r\n <button\r\n type=\"button\"\r\n class=\"fab-add-btn\"\r\n (click)=\"openPicker('excludes')\"\r\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'excludes'\"\r\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'excludes' ? 'fab-picker-region' : null\"\r\n >\r\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\r\n {{ 'audience.add_term' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (excludes().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.excludes_empty' | translate }}</div>\r\n } @else {\r\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-excludes-label\">\r\n @for (term of excludes(); track term) {\r\n <li class=\"fab-term\">\r\n <ng-container\r\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'excludes', index: $index }\">\r\n </ng-container>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Inline picker (shared across includes/excludes; gated by pickerOpen) -->\r\n @if (pickerOpen()) {\r\n <div\r\n id=\"fab-picker-region\"\r\n class=\"fab-picker\"\r\n role=\"region\"\r\n [attr.aria-label]=\"(editTarget() === 'includes'\r\n ? ('audience.adding_to_includes' | translate)\r\n : ('audience.adding_to_excludes' | translate))\"\r\n >\r\n <div class=\"fab-picker-header\">\r\n <span class=\"fab-picker-target\">\r\n {{\r\n editTarget() === 'includes'\r\n ? ('audience.adding_to_includes' | translate)\r\n : ('audience.adding_to_excludes' | translate)\r\n }}\r\n </span>\r\n <button type=\"button\" class=\"fab-close\" (click)=\"closePicker()\" [attr.aria-label]=\"'audience.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <!-- Kind tabs -->\r\n <div class=\"fab-kind-tabs\" role=\"tablist\">\r\n @for (kind of supportedKinds; track kind) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n class=\"fab-kind-tab\"\r\n [id]=\"'fab-tab-' + kind\"\r\n [class.active]=\"activeKind() === kind\"\r\n [attr.aria-selected]=\"activeKind() === kind\"\r\n [attr.aria-controls]=\"'fab-tabpanel-' + kind\"\r\n (click)=\"selectKind(kind)\"\r\n >\r\n <i [class]=\"kindIconClass(kind)\" aria-hidden=\"true\"></i>\r\n <span>{{ kindLocaleKey(kind) | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (errorKey()) {\r\n <div class=\"fab-inline-error\" role=\"alert\">\r\n <i class=\"pi pi-exclamation-triangle\" aria-hidden=\"true\"></i>\r\n {{ errorKey()! | translate }}\r\n </div>\r\n }\r\n\r\n <!-- Per-kind picker bodies -->\r\n <div\r\n class=\"fab-picker-body\"\r\n role=\"tabpanel\"\r\n [id]=\"'fab-tabpanel-' + activeKind()\"\r\n [attr.aria-labelledby]=\"'fab-tab-' + activeKind()\"\r\n >\r\n @switch (activeKind()) {\r\n\r\n @case ('users') {\r\n <input\r\n type=\"text\"\r\n class=\"fab-input\"\r\n [ngModel]=\"userSearchQuery()\"\r\n (ngModelChange)=\"userSearchQuery.set($event); onUserSearchInput()\"\r\n [placeholder]=\"'audience.search_users_placeholder' | translate\"\r\n [attr.aria-label]=\"'audience.search_users_placeholder' | translate\"\r\n />\r\n @if (userSearchResults().length > 0) {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (u of userSearchResults(); track u.id) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickUser(u)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ u.firstName }} {{ u.lastName }}</span>\r\n <span class=\"fab-result-secondary\">{{ u.email }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else if (userSearchQuery().length >= 2) {\r\n <div class=\"fab-empty\">{{ 'audience.no_users_match' | translate }}</div>\r\n }\r\n }\r\n\r\n @case ('roles') {\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <input\r\n type=\"text\"\r\n class=\"fab-input\"\r\n [ngModel]=\"roleSearchQuery()\"\r\n (ngModelChange)=\"roleSearchQuery.set($event)\"\r\n [placeholder]=\"'audience.search_roles_placeholder' | translate\"\r\n [attr.aria-label]=\"'audience.search_roles_placeholder' | translate\"\r\n />\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickRole(role)\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span>{{ role.displayName }}</span>\r\n <span class=\"fab-result-secondary\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"fab-empty\">{{ 'audience.roles_unavailable' | translate }}</div>\r\n }\r\n }\r\n\r\n @case ('ou') {\r\n @if (chartOptions().length > 0) {\r\n <label for=\"fab-chart-select\" class=\"fab-mini-label\">\r\n {{ 'audience.org_chart' | translate }}\r\n </label>\r\n <select\r\n id=\"fab-chart-select\"\r\n class=\"fab-select\"\r\n [ngModel]=\"selectedChartId()\"\r\n (ngModelChange)=\"onChartChange($event)\"\r\n >\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n }\r\n <label class=\"fab-checkbox-label\">\r\n <input\r\n type=\"checkbox\"\r\n [ngModel]=\"ouIncludeDescendants()\"\r\n (ngModelChange)=\"ouIncludeDescendants.set($event)\"\r\n />\r\n {{ 'audience.include_descendants' | translate }}\r\n </label>\r\n @if (ouTree().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.ou_tree_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list fab-ou-tree\" role=\"list\">\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n role=\"listitem\"\r\n class=\"fab-result-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"pickOu(ou)\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span>{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('app-everyone') {\r\n @if (appEveryoneOptions.length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.app_everyone_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (app of appEveryoneOptions; track app.appId) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickAppEveryone(app.appId)\">\r\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\r\n <span>{{ app.displayName }}</span>\r\n <span class=\"fab-result-secondary\">{{ app.appId }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('chart') {\r\n @if (chartTermOptions().length === 0) {\r\n <div class=\"fab-empty\">{{ 'audience.chart_options_empty' | translate }}</div>\r\n } @else {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (opt of chartTermOptions(); track opt.id) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickChart(opt.id)\">\r\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\r\n <span>{{ opt.name }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n @case ('preset') {\r\n <div class=\"fab-result-list\" role=\"list\">\r\n @for (p of presetOptions; track p) {\r\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickPreset(p)\">\r\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\r\n <span>{{ presetLocaleKey(p) | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n</div>\r\n\r\n<!-- Reusable term card template, parameterised by target bucket + index for removal callbacks -->\r\n<ng-template #termCard let-term let-target=\"target\" let-index=\"index\">\r\n <div class=\"fab-term-card\" [class.fab-term-exclude]=\"target === 'excludes'\">\r\n\r\n @switch (term.kind) {\r\n @case ('users') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_users' | translate: { count: term.userIds.length } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (id of term.userIds; track id) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ id.substring(0, 8) }}\u2026</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeUserId(target, index, id)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('roles') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_roles' | translate: { app: appLabel(term.appId), count: term.roleKeys.length } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (k of term.roleKeys; track k) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span>{{ roleLabel(term.appId, k) }}</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeRoleKey(target, index, k)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('ou') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_ous' | translate: { count: term.ouIds.length } }}\r\n @if (term.includeDescendants) {\r\n <span class=\"fab-badge\">{{ 'audience.descendants_badge' | translate }}</span>\r\n }\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n <div class=\"fab-term-chips\">\r\n @for (id of term.ouIds; track id) {\r\n <span class=\"fab-chip\">\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span>{{ ouLabel(id) }}</span>\r\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeOuId(target, index, id)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('app-everyone') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_app_everyone' | translate: { app: appLabel(term.appId) } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n\r\n @case ('chart') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ 'audience.term_chart' | translate: { chart: chartLabel(term.chartId) } }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n\r\n @case ('preset') {\r\n <div class=\"fab-term-head\">\r\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\r\n <span class=\"fab-term-title\">\r\n {{ presetLocaleKey(term.preset) | translate }}\r\n </span>\r\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\r\n [attr.aria-label]=\"'audience.remove_term' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: [".fab-root{display:flex;flex-direction:column;gap:14px;width:100%}.fab-bucket{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px}.fab-bucket.fab-bucket-exclude{border-inline-start:3px solid rgba(255,59,48,.55)}.fab-bucket-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fab-section-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-color-secondary)}.fab-add-btn{display:inline-flex;align-items:center;gap:6px;background:var(--primary-color, #e8732a);color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer}.fab-add-btn:hover{filter:brightness(1.05)}.fab-add-btn i{font-size:11px}.fab-empty{text-align:center;padding:12px;color:var(--text-color-secondary);font-size:12px;font-style:italic}.fab-term-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.fab-term-card{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px}.fab-term-card.fab-term-exclude{background:#ff3b300d;border-color:#ff3b302e}.fab-term-head{display:flex;align-items:center;gap:8px}.fab-term-head i:first-child{color:var(--text-color-secondary)}.fab-term-title{flex:1;font-size:13px;display:inline-flex;align-items:center;gap:6px}.fab-badge{font-size:10px;background:#ffffff1a;color:var(--text-color-secondary);padding:2px 6px;border-radius:8px;text-transform:uppercase;letter-spacing:.5px}.fab-term-chips{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}.fab-chip{display:inline-flex;align-items:center;gap:4px;background:#ffffff0f;border-radius:12px;padding:2px 4px 2px 8px;font-size:11px}.fab-chip i{font-size:10px;color:var(--text-color-secondary)}.fab-chip-x{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;padding:2px 4px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center}.fab-chip-x i{font-size:9px}.fab-chip-x:hover{background:#ff3b302e;color:#ff3b30}.fab-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:11px}.fab-icon-btn:hover{background:#ffffff24}.fab-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fab-picker{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 6px 18px #0000002e}.fab-picker-header{display:flex;align-items:center;justify-content:space-between}.fab-picker-target{font-size:12px;color:var(--text-color-secondary)}.fab-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:14px;padding:4px}.fab-close:hover{color:var(--text-color)}.fab-kind-tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--surface-border);padding-bottom:6px}.fab-kind-tab{background:transparent;border:1px solid transparent;border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.fab-kind-tab i{font-size:11px}.fab-kind-tab:hover{background:var(--surface-hover);color:var(--text-color)}.fab-kind-tab.active{background:#e8732a1f;border-color:var(--primary-color, #e8732a);color:var(--text-color)}.fab-picker-body{display:flex;flex-direction:column;gap:8px}.fab-input,.fab-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fab-input:focus,.fab-select:focus{border-color:var(--primary-color, #e8732a)}.fab-mini-label{font-size:11px;color:var(--text-color-secondary)}.fab-result-list{max-height:200px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fab-result-list.fab-ou-tree{max-height:240px}button.fab-result-item{width:100%;background:transparent;border:none;color:inherit;font:inherit;text-align:start;appearance:none}.fab-result-item{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:13px}.fab-result-item:hover{background:var(--surface-hover)}.fab-result-item:focus-visible{outline:2px solid var(--primary-color, #e8732a);outline-offset:-2px}.fab-result-item i{color:var(--text-color-secondary)}.fab-result-secondary{color:var(--text-color-secondary);font-size:11px;margin-inline-start:auto;text-transform:uppercase;letter-spacing:.4px}.fab-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fab-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fab-inline-error{display:flex;align-items:center;gap:6px;background:#ff3b301a;color:#ff8a80;border-radius:6px;padding:6px 10px;font-size:12px}.fab-inline-error i{font-size:12px}\n"] }]
|
|
3995
|
+
], template: "<div class=\"fab-root\">\n\n <!-- Includes section -->\n <div class=\"fab-bucket\">\n <div class=\"fab-bucket-header\">\n <div class=\"fab-section-label\" id=\"fab-includes-label\">{{ 'audience.includes' | translate }}</div>\n <button\n type=\"button\"\n class=\"fab-add-btn\"\n (click)=\"openPicker('includes')\"\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'includes'\"\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'includes' ? 'fab-picker-region' : null\"\n >\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\n {{ 'audience.add_term' | translate }}\n </button>\n </div>\n\n @if (includes().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.includes_empty' | translate }}</div>\n } @else {\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-includes-label\">\n @for (term of includes(); track term) {\n <li class=\"fab-term\">\n <ng-container\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'includes', index: $index }\">\n </ng-container>\n </li>\n }\n </ul>\n }\n </div>\n\n <!-- Excludes section -->\n @if (showExcludes) {\n <div class=\"fab-bucket fab-bucket-exclude\">\n <div class=\"fab-bucket-header\">\n <div class=\"fab-section-label\" id=\"fab-excludes-label\">{{ 'audience.excludes' | translate }}</div>\n <button\n type=\"button\"\n class=\"fab-add-btn\"\n (click)=\"openPicker('excludes')\"\n [attr.aria-expanded]=\"pickerOpen() && editTarget() === 'excludes'\"\n [attr.aria-controls]=\"pickerOpen() && editTarget() === 'excludes' ? 'fab-picker-region' : null\"\n >\n <i class=\"pi pi-plus\" aria-hidden=\"true\"></i>\n {{ 'audience.add_term' | translate }}\n </button>\n </div>\n\n @if (excludes().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.excludes_empty' | translate }}</div>\n } @else {\n <ul class=\"fab-term-list\" aria-labelledby=\"fab-excludes-label\">\n @for (term of excludes(); track term) {\n <li class=\"fab-term\">\n <ng-container\n *ngTemplateOutlet=\"termCard; context: { $implicit: term, target: 'excludes', index: $index }\">\n </ng-container>\n </li>\n }\n </ul>\n }\n </div>\n }\n\n <!-- Inline picker (shared across includes/excludes; gated by pickerOpen) -->\n @if (pickerOpen()) {\n <div\n id=\"fab-picker-region\"\n class=\"fab-picker\"\n role=\"region\"\n [attr.aria-label]=\"(editTarget() === 'includes'\n ? ('audience.adding_to_includes' | translate)\n : ('audience.adding_to_excludes' | translate))\"\n >\n <div class=\"fab-picker-header\">\n <span class=\"fab-picker-target\">\n {{\n editTarget() === 'includes'\n ? ('audience.adding_to_includes' | translate)\n : ('audience.adding_to_excludes' | translate)\n }}\n </span>\n <button type=\"button\" class=\"fab-close\" (click)=\"closePicker()\" [attr.aria-label]=\"'audience.close' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <!-- Kind tabs -->\n <div class=\"fab-kind-tabs\" role=\"tablist\">\n @for (kind of supportedKinds; track kind) {\n <button\n type=\"button\"\n role=\"tab\"\n class=\"fab-kind-tab\"\n [id]=\"'fab-tab-' + kind\"\n [class.active]=\"activeKind() === kind\"\n [attr.aria-selected]=\"activeKind() === kind\"\n [attr.aria-controls]=\"'fab-tabpanel-' + kind\"\n (click)=\"selectKind(kind)\"\n >\n <i [class]=\"kindIconClass(kind)\" aria-hidden=\"true\"></i>\n <span>{{ kindLocaleKey(kind) | translate }}</span>\n </button>\n }\n </div>\n\n @if (errorKey()) {\n <div class=\"fab-inline-error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle\" aria-hidden=\"true\"></i>\n {{ errorKey()! | translate }}\n </div>\n }\n\n <!-- Per-kind picker bodies -->\n <div\n class=\"fab-picker-body\"\n role=\"tabpanel\"\n [id]=\"'fab-tabpanel-' + activeKind()\"\n [attr.aria-labelledby]=\"'fab-tab-' + activeKind()\"\n >\n @switch (activeKind()) {\n\n @case ('users') {\n <input\n type=\"text\"\n class=\"fab-input\"\n [ngModel]=\"userSearchQuery()\"\n (ngModelChange)=\"userSearchQuery.set($event); onUserSearchInput()\"\n [placeholder]=\"'audience.search_users_placeholder' | translate\"\n [attr.aria-label]=\"'audience.search_users_placeholder' | translate\"\n />\n @if (userSearchResults().length > 0) {\n <div class=\"fab-result-list\" role=\"list\">\n @for (u of userSearchResults(); track u.id) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickUser(u)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ u.firstName }} {{ u.lastName }}</span>\n <span class=\"fab-result-secondary\">{{ u.email }}</span>\n </button>\n }\n </div>\n } @else if (userSearchQuery().length >= 2) {\n <div class=\"fab-empty\">{{ 'audience.no_users_match' | translate }}</div>\n }\n }\n\n @case ('roles') {\n @if (loadRoleOus && roleOus().length > 0) {\n <input\n type=\"text\"\n class=\"fab-input\"\n [ngModel]=\"roleSearchQuery()\"\n (ngModelChange)=\"roleSearchQuery.set($event)\"\n [placeholder]=\"'audience.search_roles_placeholder' | translate\"\n [attr.aria-label]=\"'audience.search_roles_placeholder' | translate\"\n />\n <div class=\"fab-result-list\" role=\"list\">\n @for (role of filteredRoles(); track role.ouId) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickRole(role)\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span>{{ role.displayName }}</span>\n <span class=\"fab-result-secondary\">{{ role.appId }}</span>\n </button>\n }\n @if (filteredRoles().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.no_roles_match' | translate }}</div>\n }\n </div>\n } @else {\n <div class=\"fab-empty\">{{ 'audience.roles_unavailable' | translate }}</div>\n }\n }\n\n @case ('ou') {\n @if (chartOptions().length > 0) {\n <label for=\"fab-chart-select\" class=\"fab-mini-label\">\n {{ 'audience.org_chart' | translate }}\n </label>\n <select\n id=\"fab-chart-select\"\n class=\"fab-select\"\n [ngModel]=\"selectedChartId()\"\n (ngModelChange)=\"onChartChange($event)\"\n >\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n }\n <label class=\"fab-checkbox-label\">\n <input\n type=\"checkbox\"\n [ngModel]=\"ouIncludeDescendants()\"\n (ngModelChange)=\"ouIncludeDescendants.set($event)\"\n />\n {{ 'audience.include_descendants' | translate }}\n </label>\n @if (ouTree().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.ou_tree_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list fab-ou-tree\" role=\"list\">\n @for (ou of ouTree(); track ou.id) {\n <button\n type=\"button\"\n role=\"listitem\"\n class=\"fab-result-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"pickOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('app-everyone') {\n @if (appEveryoneOptions.length === 0) {\n <div class=\"fab-empty\">{{ 'audience.app_everyone_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list\" role=\"list\">\n @for (app of appEveryoneOptions; track app.appId) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickAppEveryone(app.appId)\">\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\n <span>{{ app.displayName }}</span>\n <span class=\"fab-result-secondary\">{{ app.appId }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('chart') {\n @if (chartTermOptions().length === 0) {\n <div class=\"fab-empty\">{{ 'audience.chart_options_empty' | translate }}</div>\n } @else {\n <div class=\"fab-result-list\" role=\"list\">\n @for (opt of chartTermOptions(); track opt.id) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickChart(opt.id)\">\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\n <span>{{ opt.name }}</span>\n </button>\n }\n </div>\n }\n }\n\n @case ('preset') {\n <div class=\"fab-result-list\" role=\"list\">\n @for (p of presetOptions; track p) {\n <button type=\"button\" role=\"listitem\" class=\"fab-result-item\" (click)=\"pickPreset(p)\">\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\n <span>{{ presetLocaleKey(p) | translate }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n </div>\n }\n\n</div>\n\n<!-- Reusable term card template, parameterised by target bucket + index for removal callbacks -->\n<ng-template #termCard let-term let-target=\"target\" let-index=\"index\">\n <div class=\"fab-term-card\" [class.fab-term-exclude]=\"target === 'excludes'\">\n\n @switch (term.kind) {\n @case ('users') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_users' | translate: { count: term.userIds.length } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (id of term.userIds; track id) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ id.substring(0, 8) }}\u2026</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeUserId(target, index, id)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('roles') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_roles' | translate: { app: appLabel(term.appId), count: term.roleKeys.length } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (k of term.roleKeys; track k) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\n <span>{{ roleLabel(term.appId, k) }}</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeRoleKey(target, index, k)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('ou') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_ous' | translate: { count: term.ouIds.length } }}\n @if (term.includeDescendants) {\n <span class=\"fab-badge\">{{ 'audience.descendants_badge' | translate }}</span>\n }\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"fab-term-chips\">\n @for (id of term.ouIds; track id) {\n <span class=\"fab-chip\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ouLabel(id) }}</span>\n <button type=\"button\" class=\"fab-chip-x\" (click)=\"removeOuId(target, index, id)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </span>\n }\n </div>\n }\n\n @case ('app-everyone') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-globe\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_app_everyone' | translate: { app: appLabel(term.appId) } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n\n @case ('chart') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-share-alt\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ 'audience.term_chart' | translate: { chart: chartLabel(term.chartId) } }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n\n @case ('preset') {\n <div class=\"fab-term-head\">\n <i class=\"pi pi-star\" aria-hidden=\"true\"></i>\n <span class=\"fab-term-title\">\n {{ presetLocaleKey(term.preset) | translate }}\n </span>\n <button type=\"button\" class=\"fab-icon-btn danger\" (click)=\"removeTerm(target, index)\"\n [attr.aria-label]=\"'audience.remove_term' | translate\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n }\n </div>\n</ng-template>\n", styles: [".fab-root{display:flex;flex-direction:column;gap:14px;width:100%}.fab-bucket{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px}.fab-bucket.fab-bucket-exclude{border-inline-start:3px solid rgba(255,59,48,.55)}.fab-bucket-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fab-section-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-color-secondary)}.fab-add-btn{display:inline-flex;align-items:center;gap:6px;background:var(--primary-color, #e8732a);color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer}.fab-add-btn:hover{filter:brightness(1.05)}.fab-add-btn i{font-size:11px}.fab-empty{text-align:center;padding:12px;color:var(--text-color-secondary);font-size:12px;font-style:italic}.fab-term-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.fab-term-card{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px}.fab-term-card.fab-term-exclude{background:#ff3b300d;border-color:#ff3b302e}.fab-term-head{display:flex;align-items:center;gap:8px}.fab-term-head i:first-child{color:var(--text-color-secondary)}.fab-term-title{flex:1;font-size:13px;display:inline-flex;align-items:center;gap:6px}.fab-badge{font-size:10px;background:#ffffff1a;color:var(--text-color-secondary);padding:2px 6px;border-radius:8px;text-transform:uppercase;letter-spacing:.5px}.fab-term-chips{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}.fab-chip{display:inline-flex;align-items:center;gap:4px;background:#ffffff0f;border-radius:12px;padding:2px 4px 2px 8px;font-size:11px}.fab-chip i{font-size:10px;color:var(--text-color-secondary)}.fab-chip-x{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;padding:2px 4px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center}.fab-chip-x i{font-size:9px}.fab-chip-x:hover{background:#ff3b302e;color:#ff3b30}.fab-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:11px}.fab-icon-btn:hover{background:#ffffff24}.fab-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fab-picker{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 6px 18px #0000002e}.fab-picker-header{display:flex;align-items:center;justify-content:space-between}.fab-picker-target{font-size:12px;color:var(--text-color-secondary)}.fab-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:14px;padding:4px}.fab-close:hover{color:var(--text-color)}.fab-kind-tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--surface-border);padding-bottom:6px}.fab-kind-tab{background:transparent;border:1px solid transparent;border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.fab-kind-tab i{font-size:11px}.fab-kind-tab:hover{background:var(--surface-hover);color:var(--text-color)}.fab-kind-tab.active{background:#e8732a1f;border-color:var(--primary-color, #e8732a);color:var(--text-color)}.fab-picker-body{display:flex;flex-direction:column;gap:8px}.fab-input,.fab-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fab-input:focus,.fab-select:focus{border-color:var(--primary-color, #e8732a)}.fab-mini-label{font-size:11px;color:var(--text-color-secondary)}.fab-result-list{max-height:200px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fab-result-list.fab-ou-tree{max-height:240px}button.fab-result-item{width:100%;background:transparent;border:none;color:inherit;font:inherit;text-align:start;appearance:none}.fab-result-item{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:13px}.fab-result-item:hover{background:var(--surface-hover)}.fab-result-item:focus-visible{outline:2px solid var(--primary-color, #e8732a);outline-offset:-2px}.fab-result-item i{color:var(--text-color-secondary)}.fab-result-secondary{color:var(--text-color-secondary);font-size:11px;margin-inline-start:auto;text-transform:uppercase;letter-spacing:.4px}.fab-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fab-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fab-inline-error{display:flex;align-items:center;gap:6px;background:#ff3b301a;color:#ff8a80;border-radius:6px;padding:6px 10px;font-size:12px}.fab-inline-error i{font-size:12px}\n"] }]
|
|
2960
3996
|
}], propDecorators: { value: [{
|
|
2961
3997
|
type: Input
|
|
2962
3998
|
}], showExcludes: [{
|
|
@@ -3184,110 +4220,110 @@ class FlyImageUploadComponent {
|
|
|
3184
4220
|
});
|
|
3185
4221
|
}
|
|
3186
4222
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3187
|
-
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: `
|
|
3188
|
-
<div class="fly-image-upload">
|
|
3189
|
-
@if (previewUrl()) {
|
|
3190
|
-
<div class="fly-image-upload__preview">
|
|
3191
|
-
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
3192
|
-
<div class="fly-image-upload__overlay">
|
|
3193
|
-
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
3194
|
-
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
3195
|
-
</button>
|
|
3196
|
-
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
3197
|
-
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
3198
|
-
</button>
|
|
3199
|
-
</div>
|
|
3200
|
-
</div>
|
|
3201
|
-
} @else {
|
|
3202
|
-
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
3203
|
-
@if (uploading()) {
|
|
3204
|
-
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
3205
|
-
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
3206
|
-
} @else {
|
|
3207
|
-
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
3208
|
-
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
3209
|
-
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
3210
|
-
}
|
|
3211
|
-
</button>
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
@if (error()) {
|
|
3215
|
-
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
3219
|
-
|
|
3220
|
-
@if (showCropper()) {
|
|
3221
|
-
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
3222
|
-
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
3223
|
-
<div class="fly-image-upload__crop-header">
|
|
3224
|
-
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
3225
|
-
</div>
|
|
3226
|
-
<div class="fly-image-upload__crop-body">
|
|
3227
|
-
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
3228
|
-
</div>
|
|
3229
|
-
<div class="fly-image-upload__crop-footer">
|
|
3230
|
-
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
3231
|
-
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
3232
|
-
</div>
|
|
3233
|
-
</div>
|
|
3234
|
-
</div>
|
|
3235
|
-
}
|
|
3236
|
-
</div>
|
|
4223
|
+
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: `
|
|
4224
|
+
<div class="fly-image-upload">
|
|
4225
|
+
@if (previewUrl()) {
|
|
4226
|
+
<div class="fly-image-upload__preview">
|
|
4227
|
+
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
4228
|
+
<div class="fly-image-upload__overlay">
|
|
4229
|
+
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
4230
|
+
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
4231
|
+
</button>
|
|
4232
|
+
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
4233
|
+
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
4234
|
+
</button>
|
|
4235
|
+
</div>
|
|
4236
|
+
</div>
|
|
4237
|
+
} @else {
|
|
4238
|
+
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
4239
|
+
@if (uploading()) {
|
|
4240
|
+
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
4241
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
4242
|
+
} @else {
|
|
4243
|
+
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
4244
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
4245
|
+
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
4246
|
+
}
|
|
4247
|
+
</button>
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
@if (error()) {
|
|
4251
|
+
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
4255
|
+
|
|
4256
|
+
@if (showCropper()) {
|
|
4257
|
+
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
4258
|
+
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
4259
|
+
<div class="fly-image-upload__crop-header">
|
|
4260
|
+
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
4261
|
+
</div>
|
|
4262
|
+
<div class="fly-image-upload__crop-body">
|
|
4263
|
+
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
4264
|
+
</div>
|
|
4265
|
+
<div class="fly-image-upload__crop-footer">
|
|
4266
|
+
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
4267
|
+
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
4268
|
+
</div>
|
|
4269
|
+
</div>
|
|
4270
|
+
</div>
|
|
4271
|
+
}
|
|
4272
|
+
</div>
|
|
3237
4273
|
`, 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;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);color:var(--label-primary, #1f2937);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0006;border:1px solid var(--separator-primary, rgba(0,0,0,.1))}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1f2937)}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden;background:var(--fill-tertiary, #f3f4f6)}.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);background:var(--fill-quaternary, transparent)}html.dark-theme .fly-image-upload__crop-modal{background:var(--bg-primary, #1c1c1e);border:1px solid var(--separator-primary, rgba(255,255,255,.1))}html.dark-theme .fly-image-upload__crop-body{background:var(--fill-tertiary, #2c2c2e)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
3238
4274
|
}
|
|
3239
4275
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, decorators: [{
|
|
3240
4276
|
type: Component,
|
|
3241
|
-
args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
3242
|
-
<div class="fly-image-upload">
|
|
3243
|
-
@if (previewUrl()) {
|
|
3244
|
-
<div class="fly-image-upload__preview">
|
|
3245
|
-
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
3246
|
-
<div class="fly-image-upload__overlay">
|
|
3247
|
-
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
3248
|
-
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
3249
|
-
</button>
|
|
3250
|
-
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
3251
|
-
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
3252
|
-
</button>
|
|
3253
|
-
</div>
|
|
3254
|
-
</div>
|
|
3255
|
-
} @else {
|
|
3256
|
-
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
3257
|
-
@if (uploading()) {
|
|
3258
|
-
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
3259
|
-
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
3260
|
-
} @else {
|
|
3261
|
-
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
3262
|
-
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
3263
|
-
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
3264
|
-
}
|
|
3265
|
-
</button>
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
@if (error()) {
|
|
3269
|
-
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
3270
|
-
}
|
|
3271
|
-
|
|
3272
|
-
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
3273
|
-
|
|
3274
|
-
@if (showCropper()) {
|
|
3275
|
-
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
3276
|
-
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
3277
|
-
<div class="fly-image-upload__crop-header">
|
|
3278
|
-
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
3279
|
-
</div>
|
|
3280
|
-
<div class="fly-image-upload__crop-body">
|
|
3281
|
-
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
3282
|
-
</div>
|
|
3283
|
-
<div class="fly-image-upload__crop-footer">
|
|
3284
|
-
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
3285
|
-
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
3286
|
-
</div>
|
|
3287
|
-
</div>
|
|
3288
|
-
</div>
|
|
3289
|
-
}
|
|
3290
|
-
</div>
|
|
4277
|
+
args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
4278
|
+
<div class="fly-image-upload">
|
|
4279
|
+
@if (previewUrl()) {
|
|
4280
|
+
<div class="fly-image-upload__preview">
|
|
4281
|
+
<img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
|
|
4282
|
+
<div class="fly-image-upload__overlay">
|
|
4283
|
+
<button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
|
|
4284
|
+
<span class="pi pi-pencil" aria-hidden="true"></span>
|
|
4285
|
+
</button>
|
|
4286
|
+
<button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
|
|
4287
|
+
<span class="pi pi-trash" aria-hidden="true"></span>
|
|
4288
|
+
</button>
|
|
4289
|
+
</div>
|
|
4290
|
+
</div>
|
|
4291
|
+
} @else {
|
|
4292
|
+
<button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
|
4293
|
+
@if (uploading()) {
|
|
4294
|
+
<span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
|
|
4295
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
|
|
4296
|
+
} @else {
|
|
4297
|
+
<span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
|
|
4298
|
+
<span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
|
|
4299
|
+
<span class="fly-image-upload__hint">{{ sizeHint() }}</span>
|
|
4300
|
+
}
|
|
4301
|
+
</button>
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
@if (error()) {
|
|
4305
|
+
<div class="fly-image-upload__error">{{ error() }}</div>
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
<input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
|
|
4309
|
+
|
|
4310
|
+
@if (showCropper()) {
|
|
4311
|
+
<div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
|
|
4312
|
+
<div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
|
|
4313
|
+
<div class="fly-image-upload__crop-header">
|
|
4314
|
+
<h3>{{ 'files.imageUpload.crop' | translate }}</h3>
|
|
4315
|
+
</div>
|
|
4316
|
+
<div class="fly-image-upload__crop-body">
|
|
4317
|
+
<img #cropImage [src]="rawImageUrl()" alt="" />
|
|
4318
|
+
</div>
|
|
4319
|
+
<div class="fly-image-upload__crop-footer">
|
|
4320
|
+
<button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
|
|
4321
|
+
<button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
|
|
4322
|
+
</div>
|
|
4323
|
+
</div>
|
|
4324
|
+
</div>
|
|
4325
|
+
}
|
|
4326
|
+
</div>
|
|
3291
4327
|
`, 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;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);color:var(--label-primary, #1f2937);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0006;border:1px solid var(--separator-primary, rgba(0,0,0,.1))}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1f2937)}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden;background:var(--fill-tertiary, #f3f4f6)}.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);background:var(--fill-quaternary, transparent)}html.dark-theme .fly-image-upload__crop-modal{background:var(--bg-primary, #1c1c1e);border:1px solid var(--separator-primary, rgba(255,255,255,.1))}html.dark-theme .fly-image-upload__crop-body{background:var(--fill-tertiary, #2c2c2e)}\n"] }]
|
|
3292
4328
|
}], ctorParameters: () => [], 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 }] }] } });
|
|
3293
4329
|
|
|
@@ -3648,7 +4684,7 @@ class AgentCommandRegistry {
|
|
|
3648
4684
|
* for reactive filtering.
|
|
3649
4685
|
*/
|
|
3650
4686
|
visible(liveAppIds) {
|
|
3651
|
-
const liveSignal = isSignal
|
|
4687
|
+
const liveSignal = isSignal(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
|
|
3652
4688
|
return computed(() => {
|
|
3653
4689
|
const live = liveSignal();
|
|
3654
4690
|
return this._commands().filter((cmd) => {
|
|
@@ -3721,175 +4757,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
3721
4757
|
args: [{ providedIn: 'root' }]
|
|
3722
4758
|
}] });
|
|
3723
4759
|
/** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
|
|
3724
|
-
function isSignal$1(v) {
|
|
3725
|
-
return typeof v === 'function';
|
|
3726
|
-
}
|
|
3727
|
-
|
|
3728
|
-
/**
|
|
3729
|
-
* Singleton registry of entity lookups offered by the `/lookup` typeahead.
|
|
3730
|
-
*
|
|
3731
|
-
* Mirrors {@link AgentCommandRegistry}'s federation-singleton story
|
|
3732
|
-
* (`sharedMappings: ['@mohamedatia/fly-design-system']`), id-collision
|
|
3733
|
-
* "latest wins" contract, and disposable-handle ergonomics. OS-core entities
|
|
3734
|
-
* (note / calendar event / file) register once at shell bootstrap via
|
|
3735
|
-
* `CORE_APP_LOOKUPS`; federated remotes (Circles: scenario / trend / signal)
|
|
3736
|
-
* register at remote-component boot and dispose on window close.
|
|
3737
|
-
*
|
|
3738
|
-
* **Scope semantics diverge from commands.** Commands HIDE when their `appId`
|
|
3739
|
-
* isn't in `liveAppIds`. Lookups DO NOT — they're always offered, and
|
|
3740
|
-
* `{appId}` is just a *priority hint* that bumps that lookup to the top of
|
|
3741
|
-
* the entity picker when the app is live. See {@link LookupRegistration.scope}
|
|
3742
|
-
* for the rationale.
|
|
3743
|
-
*
|
|
3744
|
-
* Storage is a signal store keyed on {@link LookupRegistration.entity}. Because
|
|
3745
|
-
* `entity` is the collision key, an app re-registering the same entity replaces
|
|
3746
|
-
* the prior descriptor; a stale handle's `dispose()` then no-ops.
|
|
3747
|
-
*/
|
|
3748
|
-
class AgentLookupRegistry {
|
|
3749
|
-
_lookups = signal([], ...(ngDevMode ? [{ debugName: "_lookups" }] : /* istanbul ignore next */ []));
|
|
3750
|
-
/** All currently-registered lookups, in insertion order. */
|
|
3751
|
-
all = this._lookups.asReadonly();
|
|
3752
|
-
/**
|
|
3753
|
-
* All registered lookups, sorted by affinity to `liveAppIds`:
|
|
3754
|
-
*
|
|
3755
|
-
* 1. Lookups whose `scope.appId` is in the live app set (in registration
|
|
3756
|
-
* order within that bucket).
|
|
3757
|
-
* 2. Then everything else — `'global'` lookups AND scoped lookups whose
|
|
3758
|
-
* app isn't currently live — in registration order.
|
|
3759
|
-
*
|
|
3760
|
-
* Recomputes when either the registry or `liveAppIds` changes. Pass a
|
|
3761
|
-
* `Signal<ReadonlySet<string>>` from the host's app-registry for reactive
|
|
3762
|
-
* re-sorting. **Always returns the full registry** — see the type doc on
|
|
3763
|
-
* {@link LookupRegistration.scope} for why this differs from
|
|
3764
|
-
* {@link AgentCommandRegistry.visible}.
|
|
3765
|
-
*/
|
|
3766
|
-
visible(liveAppIds) {
|
|
3767
|
-
const liveSignal = isSignal(liveAppIds)
|
|
3768
|
-
? liveAppIds
|
|
3769
|
-
: signal(liveAppIds).asReadonly();
|
|
3770
|
-
return computed(() => {
|
|
3771
|
-
const live = liveSignal();
|
|
3772
|
-
const all = this._lookups();
|
|
3773
|
-
const prioritized = [];
|
|
3774
|
-
const rest = [];
|
|
3775
|
-
for (const l of all) {
|
|
3776
|
-
if (l.scope !== 'global' && live.has(l.scope.appId)) {
|
|
3777
|
-
prioritized.push(l);
|
|
3778
|
-
}
|
|
3779
|
-
else {
|
|
3780
|
-
rest.push(l);
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
return prioritized.length === 0 ? all : [...prioritized, ...rest];
|
|
3784
|
-
});
|
|
3785
|
-
}
|
|
3786
|
-
/**
|
|
3787
|
-
* Register one lookup. Returns a handle whose `dispose()` removes the row by
|
|
3788
|
-
* `entity`. A later re-registration of the same entity makes the original
|
|
3789
|
-
* handle's `dispose()` a no-op (the newer registration owns the row).
|
|
3790
|
-
*/
|
|
3791
|
-
register(lookup) {
|
|
3792
|
-
const generation = ++this._generation;
|
|
3793
|
-
this._lookups.update((rows) => [
|
|
3794
|
-
...rows.filter((r) => r.entity !== lookup.entity),
|
|
3795
|
-
lookup,
|
|
3796
|
-
]);
|
|
3797
|
-
this._owners.set(lookup.entity, generation);
|
|
3798
|
-
return {
|
|
3799
|
-
dispose: () => {
|
|
3800
|
-
if (this._owners.get(lookup.entity) === generation) {
|
|
3801
|
-
this._owners.delete(lookup.entity);
|
|
3802
|
-
this._lookups.update((rows) => rows.filter((r) => r.entity !== lookup.entity));
|
|
3803
|
-
}
|
|
3804
|
-
},
|
|
3805
|
-
};
|
|
3806
|
-
}
|
|
3807
|
-
/**
|
|
3808
|
-
* Bulk register. Rolls back on a duplicate entity WITHIN the input batch
|
|
3809
|
-
* (throws before any row lands). Cross-batch duplicates against existing rows
|
|
3810
|
-
* follow the standard "latest wins" rule and do NOT trigger rollback.
|
|
3811
|
-
*/
|
|
3812
|
-
registerAll(lookups) {
|
|
3813
|
-
const seen = new Set();
|
|
3814
|
-
for (const l of lookups) {
|
|
3815
|
-
if (seen.has(l.entity)) {
|
|
3816
|
-
throw new Error(`AgentLookupRegistry.registerAll: duplicate entity "${l.entity}" in batch`);
|
|
3817
|
-
}
|
|
3818
|
-
seen.add(l.entity);
|
|
3819
|
-
}
|
|
3820
|
-
const handles = lookups.map((l) => this.register(l));
|
|
3821
|
-
let disposed = false;
|
|
3822
|
-
return {
|
|
3823
|
-
dispose: () => {
|
|
3824
|
-
if (disposed)
|
|
3825
|
-
return;
|
|
3826
|
-
disposed = true;
|
|
3827
|
-
for (const h of handles)
|
|
3828
|
-
h.dispose();
|
|
3829
|
-
},
|
|
3830
|
-
};
|
|
3831
|
-
}
|
|
3832
|
-
/**
|
|
3833
|
-
* Resolve a deep-link anchor to a concrete launch target.
|
|
3834
|
-
*
|
|
3835
|
-
* `kind` is the dotted `<appId>.<entity>` token the agents backend emits
|
|
3836
|
-
* inside `flyos:<kind>/<id>` chat-answer anchors — the same entity-kind
|
|
3837
|
-
* vocabulary as drag-payload kinds and `ref` parts. Returns
|
|
3838
|
-
* `{ appId, route }` when a registered lookup for that `(appId, entity)`
|
|
3839
|
-
* pair carries a {@link LookupDescriptor.deepLinkRoute} template; `null`
|
|
3840
|
-
* otherwise (unknown entity, app mismatch, or no template — e.g. the
|
|
3841
|
-
* owning app isn't installed) so the caller renders plain text rather than
|
|
3842
|
-
* a dead link.
|
|
3843
|
-
*
|
|
3844
|
-
* `appId` and `entity` are both dot-free by their own grammars, so the
|
|
3845
|
-
* FIRST dot is the unambiguous split point; a dotless `kind` can't carry an
|
|
3846
|
-
* app and never resolves. The template's single `{id}` placeholder is
|
|
3847
|
-
* substituted URL-encoded.
|
|
3848
|
-
*/
|
|
3849
|
-
resolveDeepLink(kind, id) {
|
|
3850
|
-
if (!kind || !id)
|
|
3851
|
-
return null;
|
|
3852
|
-
const dot = kind.indexOf('.');
|
|
3853
|
-
if (dot <= 0 || dot >= kind.length - 1)
|
|
3854
|
-
return null;
|
|
3855
|
-
const appId = kind.slice(0, dot);
|
|
3856
|
-
const entity = kind.slice(dot + 1);
|
|
3857
|
-
const reg = this._lookups().find((r) => r.entity === entity && r.deepLinkRoute && ownerAppId(r) === appId);
|
|
3858
|
-
if (!reg?.deepLinkRoute)
|
|
3859
|
-
return null;
|
|
3860
|
-
return { appId, route: reg.deepLinkRoute.replace('{id}', encodeURIComponent(id)) };
|
|
3861
|
-
}
|
|
3862
|
-
/** Tear down by entity. Idempotent. */
|
|
3863
|
-
unregister(entity) {
|
|
3864
|
-
if (this._owners.delete(entity)) {
|
|
3865
|
-
this._lookups.update((rows) => rows.filter((r) => r.entity !== entity));
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
/** Monotonic counter; identifies which registration call currently owns each entity. */
|
|
3869
|
-
_generation = 0;
|
|
3870
|
-
/** entity → generation. Lets a stale handle's `dispose()` no-op after replacement. */
|
|
3871
|
-
_owners = new Map();
|
|
3872
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3873
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, providedIn: 'root' });
|
|
3874
|
-
}
|
|
3875
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, decorators: [{
|
|
3876
|
-
type: Injectable,
|
|
3877
|
-
args: [{ providedIn: 'root' }]
|
|
3878
|
-
}] });
|
|
3879
|
-
/** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
|
|
3880
4760
|
function isSignal(v) {
|
|
3881
4761
|
return typeof v === 'function';
|
|
3882
4762
|
}
|
|
3883
|
-
/**
|
|
3884
|
-
* The app that owns a registration, for deep-link resolution. Prefers the
|
|
3885
|
-
* stamped {@link LookupDescriptor.appId} (set by `RemoteManifestService` /
|
|
3886
|
-
* `CORE_APP_LOOKUPS`), falling back to the affinity `scope.appId` so a raw
|
|
3887
|
-
* manifest descriptor still resolves even before the host stamps it.
|
|
3888
|
-
* Returns `undefined` for `'global'`-scoped lookups with no stamped appId.
|
|
3889
|
-
*/
|
|
3890
|
-
function ownerAppId(reg) {
|
|
3891
|
-
return reg.appId ?? (typeof reg.scope === 'object' ? reg.scope.appId : undefined);
|
|
3892
|
-
}
|
|
3893
4763
|
|
|
3894
4764
|
/**
|
|
3895
4765
|
* Singleton registry of chip-renderer components and keyboard-alternative draggable
|
|
@@ -5628,5 +6498,5 @@ const AUDIENCE_ERROR_CODES = {
|
|
|
5628
6498
|
* Generated bundle index. Do not edit.
|
|
5629
6499
|
*/
|
|
5630
6500
|
|
|
5631
|
-
export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyWindowHelpService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
|
|
6501
|
+
export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyMarkdownEditorComponent, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyWindowHelpService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MARKDOWN_TOOLBAR_PRESETS, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
|
|
5632
6502
|
//# sourceMappingURL=mohamedatia-fly-design-system.mjs.map
|