wuic-framework-lib 1.3.0 → 1.5.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.
Files changed (21) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-C02jeFGy.mjs → wuic-framework-lib-chart-list.component-0J1ghIrS.mjs} +4 -4
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-BZNLr0Jj.mjs → wuic-framework-lib-code-editor.component-C21mvjyr.mjs} +277 -13
  3. package/fesm2022/{wuic-framework-lib-designer.component-N_CfbGnr.mjs → wuic-framework-lib-designer.component-x10ZzUEy.mjs} +1649 -184
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-lW7-xMm3.mjs → wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs} +83 -43
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-CGQbmSkv.mjs → wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs} +1326 -99
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-0gJz9RWr.mjs → wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs} +6 -6
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-C9LvmFsL.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs} +2 -2
  8. package/fesm2022/{wuic-framework-lib-report-designer.component-DcpdzFjh.mjs → wuic-framework-lib-report-designer.component-ZvB46-LV.mjs} +2 -2
  9. package/fesm2022/{wuic-framework-lib-report-viewer.component-BHpbGpp9.mjs → wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs} +2 -2
  10. package/fesm2022/wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs +4578 -0
  11. package/fesm2022/wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs +303 -0
  12. package/fesm2022/wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs +2227 -0
  13. package/fesm2022/wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs +999 -0
  14. package/fesm2022/{wuic-framework-lib-scheduler-list.component-DHoukjf-.mjs → wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs} +2 -2
  15. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-VsaXepUC.mjs → wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs} +4 -4
  16. package/fesm2022/wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs +9474 -0
  17. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-Deudd4MY.mjs → wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs} +2682 -732
  18. package/fesm2022/wuic-framework-lib.mjs +1 -1
  19. package/package.json +2 -1
  20. package/types/wuic-framework-lib.d.ts +762 -9
  21. package/fesm2022/wuic-framework-lib-workflow-designer.component-epj3oKq3.mjs +0 -5114
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Pipe, EventEmitter, ElementRef, Renderer2, ChangeDetectorRef, HostListener, Input, Output, Inject, Directive, Component, computed, signal, untracked, inject, NgZone, numberAttribute, DOCUMENT, input, output, isDevMode, InjectionToken, Injector, contentChildren, contentChild, booleanAttribute, effect, HostBinding, ChangeDetectionStrategy } from '@angular/core';
2
+ import { Pipe, EventEmitter, ElementRef, Renderer2, ChangeDetectorRef, HostListener, Input, Output, Inject, Directive, Component, computed, signal, untracked, inject, NgZone, numberAttribute, DOCUMENT, input, output, isDevMode, InjectionToken, Injector, contentChildren, contentChild, booleanAttribute, effect, HostBinding, ChangeDetectionStrategy, ViewChild } from '@angular/core';
3
3
  import * as i10 from 'primeng/splitter';
4
4
  import { SplitterModule } from 'primeng/splitter';
5
5
  import * as i12 from 'primeng/dragdrop';
6
6
  import { DragDropModule } from 'primeng/dragdrop';
7
7
  import { NgClass, NgStyle, NgTemplateOutlet, AsyncPipe, JsonPipe, NgFor, NgIf, NgComponentOutlet } from '@angular/common';
8
- import { W as WtoolboxService, D as DynamicGenericTemplateComponent, a as DynamicCompilerService, b as DataSourceComponent, c as DataRepeaterComponent, F as FilterBarComponent, L as LazyParametricDialogComponent, P as PagerComponent, M as MetadataProviderService, d as ParametricDialogComponent, e as MetadatiColonna, f as DataProviderService, U as UserInfoService, T as TranslationManagerService, g as MetadataEditorService, C as ChatbotHostRegistryService, h as WorkflowRuntimeMetadataService, i as FieldEditorComponent, j as MetadataEditorComponent, A as ArchetypeConfiguratorComponent } from './wuic-framework-lib-wuic-framework-lib-Deudd4MY.mjs';
8
+ import { W as WtoolboxService, D as DynamicGenericTemplateComponent, a as DynamicCompilerService, b as DataSourceComponent, c as DataRepeaterComponent, F as FilterBarComponent, L as LazyParametricDialogComponent, P as PagerComponent, M as MetadataProviderService, d as ParametricDialogComponent, e as MetadatiColonna, f as DataProviderService, U as UserInfoService, T as TranslationManagerService, g as MetadataEditorService, C as ChatbotHostRegistryService, h as WorkflowRuntimeMetadataService, i as FieldEditorComponent, j as MetadataEditorComponent, A as ArchetypeConfiguratorComponent } from './wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs';
9
9
  import { fromEvent, BehaviorSubject, merge, filter, Observable, switchMap, take, map, takeUntil, tap, timeInterval, scan, mergeMap, of, delay, repeat, Subject, startWith, pairwise, skipWhile, combineLatest } from 'rxjs';
10
10
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
11
11
  import { CdkAccordionModule, CdkAccordion, CdkAccordionItem } from '@angular/cdk/accordion';
@@ -17,12 +17,12 @@ import * as i2$1 from 'primeng/colorpicker';
17
17
  import { ColorPickerModule } from 'primeng/colorpicker';
18
18
  import * as i2$2 from 'primeng/autocomplete';
19
19
  import { AutoCompleteModule } from 'primeng/autocomplete';
20
- import * as i8 from '@ngx-translate/core';
20
+ import * as i2$3 from '@ngx-translate/core';
21
21
  import { TranslateModule } from '@ngx-translate/core';
22
22
  import * as i1$1 from '@angular/router';
23
23
  import { UrlSegmentGroup, UrlTree, ActivatedRouteSnapshot, RouterStateSnapshot, RouterState, ActivatedRoute } from '@angular/router';
24
24
  import { SafeSubscriber } from 'rxjs/internal/Subscriber';
25
- import * as i14 from 'primeng/contextmenu';
25
+ import * as i10$1 from 'primeng/contextmenu';
26
26
  import { ContextMenuModule } from 'primeng/contextmenu';
27
27
  import * as i3 from 'primeng/api';
28
28
  import { TreeDragDropService } from 'primeng/api';
@@ -33,7 +33,7 @@ import * as i18 from 'primeng/multiselect';
33
33
  import { MultiSelectModule } from 'primeng/multiselect';
34
34
  import * as i2 from 'primeng/dialog';
35
35
  import { DialogModule } from 'primeng/dialog';
36
- import * as i9 from 'primeng/menu';
36
+ import * as i7 from 'primeng/menu';
37
37
  import { MenuModule } from 'primeng/menu';
38
38
  import * as i4$1 from 'primeng/tree';
39
39
  import { TreeModule } from 'primeng/tree';
@@ -439,6 +439,23 @@ class NgxDraggableDomUtilities {
439
439
  }
440
440
  }
441
441
 
442
+ /**
443
+ * Stato condiviso dello snap-to-grid del designer.
444
+ *
445
+ * Sorgente di verita' NEUTRA (nessun import Angular) letta dalle direttive di
446
+ * drag (`NgxDraggableDomDirective`) e resize (`AngularResizableDirective`) e
447
+ * scritta da `DesignerComponent.toggleSnapToGrid`. Serve perche' quelle
448
+ * direttive sono applicate agli elementi via TEMPLATE DINAMICO generato
449
+ * (`DynamicDashboardTemplateComponent` riscrive `ngxDraggableDom="true"`), dove
450
+ * non e' pratico bindare un `[snapGridSize]` per-elemento. Un modulo neutro
451
+ * evita anche l'import circolare direttiva <-> componente.
452
+ *
453
+ * `gridSize` = passo in px quando lo snap e' attivo, 0 = snap off.
454
+ */
455
+ const DesignerSnapState = {
456
+ gridSize: 0,
457
+ };
458
+
442
459
  class NgxDraggableDomDirective {
443
460
  el;
444
461
  renderer;
@@ -840,6 +857,16 @@ class NgxDraggableDomDirective {
840
857
  translation = boundsCheck.translation;
841
858
  }
842
859
  }
860
+ // Designer snap-to-grid: arrotonda la posizione ASSOLUTA dell'elemento al
861
+ // multiplo di griglia, poi riporta il delta nel translation. `offsetLeft`/
862
+ // `offsetTop` NON includono il transform, quindi (offset + translation) e' la
863
+ // posizione visiva reale sul canvas (offsetParent). No-op se snap off.
864
+ const _snapGrid = DesignerSnapState.gridSize;
865
+ if (_snapGrid && _snapGrid > 0) {
866
+ const _dragEl = this.el.nativeElement;
867
+ translation.x = Math.round((_dragEl.offsetLeft + translation.x) / _snapGrid) * _snapGrid - _dragEl.offsetLeft;
868
+ translation.y = Math.round((_dragEl.offsetTop + translation.y) / _snapGrid) * _snapGrid - _dragEl.offsetTop;
869
+ }
843
870
  // if it is possible, get the transform from the computed style and modify the matrix to maintain transform properties
844
871
  if (window) {
845
872
  // update the x and y values as part of the matrix
@@ -2048,6 +2075,12 @@ class AngularResizableDirective {
2048
2075
  this._gridSize = { x: this.rzGrid[0], y: this.rzGrid[1] };
2049
2076
  }
2050
2077
  }
2078
+ else if (DesignerSnapState.gridSize && DesignerSnapState.gridSize > 0) {
2079
+ // Designer snap-to-grid (stato condiviso): usa il passo griglia quando lo
2080
+ // snap e' attivo e nessun `rzGrid` esplicito e' bindato (il designer
2081
+ // applica la direttiva via template dinamico, senza binding per-elemento).
2082
+ this._gridSize = { x: DesignerSnapState.gridSize, y: DesignerSnapState.gridSize };
2083
+ }
2051
2084
  }
2052
2085
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AngularResizableDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
2053
2086
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: AngularResizableDirective, isStandalone: true, selector: "[ngResizable]", inputs: { ngResizable: "ngResizable", rzHandles: "rzHandles", rzHandleDoms: "rzHandleDoms", rzAspectRatio: "rzAspectRatio", rzContainment: "rzContainment", rzGrid: "rzGrid", rzMinWidth: "rzMinWidth", rzMinHeight: "rzMinHeight", rzMaxWidth: "rzMaxWidth", rzMaxHeight: "rzMaxHeight", rzScale: "rzScale", preventDefaultEvent: "preventDefaultEvent" }, outputs: { rzStart: "rzStart", rzResizing: "rzResizing", rzStop: "rzStop" }, usesOnChanges: true, ngImport: i0 });
@@ -2202,11 +2235,11 @@ class TabViewWrapperComponent {
2202
2235
  });
2203
2236
  }
2204
2237
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TabViewWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2205
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: TabViewWrapperComponent, isStandalone: true, selector: "p-tabView", inputs: { tabs: "tabs", activeIndex: "activeIndex" }, ngImport: i0, template: "<div data-pc-name=\"tabview\" class=\"p-tabview p-component\">\n <div class=\"p-tabview-nav-container\">\n <div class=\"p-tabview-nav-content\" data-pc-section=\"navcontent\">\n <ul role=\"tablist\" class=\"p-tabview-nav\" data-pc-section=\"nav\">\n @for (tab of tabs; track tab; let i = $index) {\n <li role=\"presentation\" data-p-disabled=\"false\"\n [ngClass]=\"{'p-highlight': activeIndex == i}\">\n <button type=\"button\" role=\"tab\" pripple=\"\" class=\"p-element p-ripple p-tabview-nav-link\" (click)=\"setIndex(i)\"\n [attr.aria-selected]=\"activeIndex == i\" [attr.tabindex]=\"i\" aria-disabled=\"false\"\n [attr.data-pc-index]=\"i\" data-pc-section=\"headeraction\">\n <span class=\"p-tabview-title\">\n {{tab.inputs.header}}\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n <div class=\"p-tabview-panels\">\n <ng-content></ng-content>\n </div>\n</div>", styles: [".p-tabview.p-component{height:100%}.p-tabview .p-tabview-nav-content{scroll-padding-inline:3rem}.p-tabview-nav-content{overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior:contain auto}.p-tabview-nav{display:inline-flex;min-width:100%;margin:0;padding:0;list-style-type:none;flex:1 1 auto}.p-tabview .p-tabview-nav li .p-tabview-nav-link{border-width:0 0 2px 0;padding:1.25rem;font-weight:700;border-top-right-radius:6px;border-top-left-radius:6px;transition:box-shadow .2s;margin:0 0 -2px}.p-tabview-nav-link{appearance:none;background:transparent;border:none;font:inherit;color:inherit;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;position:relative;text-decoration:none;overflow:hidden;width:100%}.p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link{border-color:var(--p-primary-color, #3B82F6);color:var(--p-primary-color, #3B82F6);background-color:transparent}.p-tabview-panels{height:calc(100% - 60px);overflow:auto}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
2238
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: TabViewWrapperComponent, isStandalone: true, selector: "p-tabView", inputs: { tabs: "tabs", activeIndex: "activeIndex" }, ngImport: i0, template: "<div data-pc-name=\"tabview\" class=\"p-tabview p-component\">\n <div class=\"p-tabview-nav-container\">\n <div class=\"p-tabview-nav-content\" data-pc-section=\"navcontent\">\n <ul role=\"tablist\" class=\"p-tabview-nav\" data-pc-section=\"nav\">\n @for (tab of tabs; track tab; let i = $index) {\n <li role=\"presentation\" data-p-disabled=\"false\"\n [ngClass]=\"{'p-highlight': tab.inputs?.selected}\">\n <button type=\"button\" role=\"tab\" pripple=\"\" class=\"p-element p-ripple p-tabview-nav-link\" (click)=\"setIndex(i)\"\n [attr.aria-selected]=\"tab.inputs?.selected\" [attr.tabindex]=\"i\" aria-disabled=\"false\"\n [attr.data-pc-index]=\"i\" data-pc-section=\"headeraction\">\n <span class=\"p-tabview-title\">\n {{tab.inputs.header}}\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n <div class=\"p-tabview-panels\">\n <ng-content></ng-content>\n </div>\n</div>", styles: [".p-tabview.p-component{height:100%}.p-tabview .p-tabview-nav-content{scroll-padding-inline:3rem}.p-tabview-nav-content{overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior:contain auto}.p-tabview-nav{display:inline-flex;min-width:100%;margin:0;padding:0;list-style-type:none;flex:1 1 auto}.p-tabview .p-tabview-nav li .p-tabview-nav-link{border-width:0 0 2px 0;padding:1.25rem;font-weight:700;border-top-right-radius:6px;border-top-left-radius:6px;transition:box-shadow .2s;margin:0 0 -2px}.p-tabview-nav-link{appearance:none;background:transparent;border:none;font:inherit;color:inherit;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;position:relative;text-decoration:none;overflow:hidden;width:100%}.p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link{border-color:var(--p-primary-color, #3B82F6);color:var(--p-primary-color, #3B82F6);background-color:transparent}.p-tabview-panels{height:calc(100% - 60px);overflow:auto}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
2206
2239
  }
2207
2240
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TabViewWrapperComponent, decorators: [{
2208
2241
  type: Component,
2209
- args: [{ selector: 'p-tabView', imports: [NgClass], template: "<div data-pc-name=\"tabview\" class=\"p-tabview p-component\">\n <div class=\"p-tabview-nav-container\">\n <div class=\"p-tabview-nav-content\" data-pc-section=\"navcontent\">\n <ul role=\"tablist\" class=\"p-tabview-nav\" data-pc-section=\"nav\">\n @for (tab of tabs; track tab; let i = $index) {\n <li role=\"presentation\" data-p-disabled=\"false\"\n [ngClass]=\"{'p-highlight': activeIndex == i}\">\n <button type=\"button\" role=\"tab\" pripple=\"\" class=\"p-element p-ripple p-tabview-nav-link\" (click)=\"setIndex(i)\"\n [attr.aria-selected]=\"activeIndex == i\" [attr.tabindex]=\"i\" aria-disabled=\"false\"\n [attr.data-pc-index]=\"i\" data-pc-section=\"headeraction\">\n <span class=\"p-tabview-title\">\n {{tab.inputs.header}}\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n <div class=\"p-tabview-panels\">\n <ng-content></ng-content>\n </div>\n</div>", styles: [".p-tabview.p-component{height:100%}.p-tabview .p-tabview-nav-content{scroll-padding-inline:3rem}.p-tabview-nav-content{overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior:contain auto}.p-tabview-nav{display:inline-flex;min-width:100%;margin:0;padding:0;list-style-type:none;flex:1 1 auto}.p-tabview .p-tabview-nav li .p-tabview-nav-link{border-width:0 0 2px 0;padding:1.25rem;font-weight:700;border-top-right-radius:6px;border-top-left-radius:6px;transition:box-shadow .2s;margin:0 0 -2px}.p-tabview-nav-link{appearance:none;background:transparent;border:none;font:inherit;color:inherit;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;position:relative;text-decoration:none;overflow:hidden;width:100%}.p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link{border-color:var(--p-primary-color, #3B82F6);color:var(--p-primary-color, #3B82F6);background-color:transparent}.p-tabview-panels{height:calc(100% - 60px);overflow:auto}\n"] }]
2242
+ args: [{ selector: 'p-tabView', imports: [NgClass], template: "<div data-pc-name=\"tabview\" class=\"p-tabview p-component\">\n <div class=\"p-tabview-nav-container\">\n <div class=\"p-tabview-nav-content\" data-pc-section=\"navcontent\">\n <ul role=\"tablist\" class=\"p-tabview-nav\" data-pc-section=\"nav\">\n @for (tab of tabs; track tab; let i = $index) {\n <li role=\"presentation\" data-p-disabled=\"false\"\n [ngClass]=\"{'p-highlight': tab.inputs?.selected}\">\n <button type=\"button\" role=\"tab\" pripple=\"\" class=\"p-element p-ripple p-tabview-nav-link\" (click)=\"setIndex(i)\"\n [attr.aria-selected]=\"tab.inputs?.selected\" [attr.tabindex]=\"i\" aria-disabled=\"false\"\n [attr.data-pc-index]=\"i\" data-pc-section=\"headeraction\">\n <span class=\"p-tabview-title\">\n {{tab.inputs.header}}\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n <div class=\"p-tabview-panels\">\n <ng-content></ng-content>\n </div>\n</div>", styles: [".p-tabview.p-component{height:100%}.p-tabview .p-tabview-nav-content{scroll-padding-inline:3rem}.p-tabview-nav-content{overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior:contain auto}.p-tabview-nav{display:inline-flex;min-width:100%;margin:0;padding:0;list-style-type:none;flex:1 1 auto}.p-tabview .p-tabview-nav li .p-tabview-nav-link{border-width:0 0 2px 0;padding:1.25rem;font-weight:700;border-top-right-radius:6px;border-top-left-radius:6px;transition:box-shadow .2s;margin:0 0 -2px}.p-tabview-nav-link{appearance:none;background:transparent;border:none;font:inherit;color:inherit;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center;position:relative;text-decoration:none;overflow:hidden;width:100%}.p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link{border-color:var(--p-primary-color, #3B82F6);color:var(--p-primary-color, #3B82F6);background-color:transparent}.p-tabview-panels{height:calc(100% - 60px);overflow:auto}\n"] }]
2210
2243
  }], propDecorators: { tabs: [{
2211
2244
  type: Input
2212
2245
  }], activeIndex: [{
@@ -3352,6 +3385,23 @@ class DynamicDashboardTemplateComponent {
3352
3385
  * does not trigger a spurious commit.
3353
3386
  */
3354
3387
  static lastDesignerGestureTimestamp = 0;
3388
+ /**
3389
+ * Dimensione della griglia di snap (px) per drag/resize nel designer.
3390
+ * 0 = snap disattivato (comportamento libero, default). Impostato staticamente
3391
+ * da DesignerComponent quando l'utente attiva "snap alla griglia": statico per
3392
+ * lo stesso motivo di lastDesignerGestureTimestamp (ogni elemento ha la propria
3393
+ * istanza di template). onMoving/onResizing arrotondano posizione/dimensione al
3394
+ * multiplo piu' vicino quando > 0.
3395
+ */
3396
+ static snapGridSize = 0;
3397
+ /** Arrotonda un valore al multiplo di `snapGridSize` (no-op se snap off). */
3398
+ static snap(value) {
3399
+ const g = DynamicDashboardTemplateComponent.snapGridSize;
3400
+ if (!g || g <= 0) {
3401
+ return value;
3402
+ }
3403
+ return Math.round(value / g) * g;
3404
+ }
3355
3405
  /**
3356
3406
  * Input dal componente padre per inputs; usata nella configurazione e nel rendering del componente.
3357
3407
  */
@@ -3460,8 +3510,8 @@ class DynamicDashboardTemplateComponent {
3460
3510
  }
3461
3511
  else {
3462
3512
  }
3463
- element.inputs.width = event.size.width + 'px';
3464
- element.inputs.height = event.size.height + 'px';
3513
+ element.inputs.width = DynamicDashboardTemplateComponent.snap(event.size.width) + 'px';
3514
+ element.inputs.height = DynamicDashboardTemplateComponent.snap(event.size.height) + 'px';
3465
3515
  // Mark "designer mutation in progress" so the next window:mouseup in
3466
3516
  // DesignerComponent commits a history snapshot. See the field doc.
3467
3517
  DynamicDashboardTemplateComponent.lastDesignerGestureTimestamp = Date.now();
@@ -3492,6 +3542,11 @@ class DynamicDashboardTemplateComponent {
3492
3542
  scaleY = matrix[3];
3493
3543
  }
3494
3544
  }
3545
+ // La direttiva ngx-draggable-dom snappa gia' la traslazione alla griglia
3546
+ // ASSOLUTA (offsetLeft/Top + delta), quindi $event.position e' gia' allineato.
3547
+ // NON ri-snappare con DynamicDashboardTemplateComponent.snap(): e' un
3548
+ // arrotondamento grid-STEP che, applicato a una traslazione gia' assoluta,
3549
+ // la disallineava (l'offset visto sui box trascinati).
3495
3550
  element.inputs.transform = `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${$event.position.x}, ${$event.position.y})`;
3496
3551
  // Mark "designer mutation in progress" so the next window:mouseup in
3497
3552
  // DesignerComponent commits a history snapshot. See the field doc.
@@ -3724,6 +3779,24 @@ class DashboardComponent {
3724
3779
  * Input dal componente padre per dashboard elements; usata nella configurazione e nel rendering del componente.
3725
3780
  */
3726
3781
  dashboardElements;
3782
+ /**
3783
+ * Layout assoluto vs flusso normale: il "modo" con cui la dashboard e' stata
3784
+ * SALVATA e' codificato per-elemento nel boardcontent (`inputs.position`).
3785
+ * Se almeno un figlio diretto e' `absolute`/`fixed`, questo host deve diventare
3786
+ * il contesto di posizionamento dei figli (altrimenti risalgono al viewport e
3787
+ * finiscono sopra il menu fixed). In flusso normale la classe NON viene
3788
+ * applicata -> nessun cambiamento di layout, retrocompatibile.
3789
+ *
3790
+ * Vale sia top-level (host = area dashboard sotto il menu) sia annidato
3791
+ * (host = dentro il container `position:relative` gia' salvato dal designer):
3792
+ * in entrambi i casi il figlio absolute risolve left/top rispetto a questo box.
3793
+ */
3794
+ get hasAbsoluteLayout() {
3795
+ return (this.dashboardElements || []).some((el) => {
3796
+ const pos = String(el?.inputs?.position || '').trim().toLowerCase();
3797
+ return pos === 'absolute' || pos === 'fixed';
3798
+ });
3799
+ }
3727
3800
  /**
3728
3801
  * function Object() { [native code] }
3729
3802
  */
@@ -3755,13 +3828,16 @@ class DashboardComponent {
3755
3828
  }
3756
3829
  ;
3757
3830
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3758
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardComponent, isStandalone: true, selector: "wuic-dashboard", inputs: { dashboardElements: "dashboardElements" }, ngImport: i0, template: "@for (dashboardElement of dashboardElements; track dashboardElement.uniqueName) {\r\n<ng-container *ngComponentOutlet=\"getComponent(dashboardElement); inputs: getComponentInputs(dashboardElement)\"></ng-container>\r\n}\r\n", styles: [""], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }] });
3831
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardComponent, isStandalone: true, selector: "wuic-dashboard", inputs: { dashboardElements: "dashboardElements" }, host: { properties: { "class.wuic-dashboard-absolute-layout": "this.hasAbsoluteLayout" } }, ngImport: i0, template: "@for (dashboardElement of dashboardElements; track dashboardElement.uniqueName) {\r\n<ng-container *ngComponentOutlet=\"getComponent(dashboardElement); inputs: getComponentInputs(dashboardElement)\"></ng-container>\r\n}\r\n", styles: [":host(.wuic-dashboard-absolute-layout){position:relative;display:block;width:100%;height:100%}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }] });
3759
3832
  }
3760
3833
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardComponent, decorators: [{
3761
3834
  type: Component,
3762
- args: [{ selector: 'wuic-dashboard', imports: [NgComponentOutlet], template: "@for (dashboardElement of dashboardElements; track dashboardElement.uniqueName) {\r\n<ng-container *ngComponentOutlet=\"getComponent(dashboardElement); inputs: getComponentInputs(dashboardElement)\"></ng-container>\r\n}\r\n" }]
3835
+ args: [{ selector: 'wuic-dashboard', imports: [NgComponentOutlet], template: "@for (dashboardElement of dashboardElements; track dashboardElement.uniqueName) {\r\n<ng-container *ngComponentOutlet=\"getComponent(dashboardElement); inputs: getComponentInputs(dashboardElement)\"></ng-container>\r\n}\r\n", styles: [":host(.wuic-dashboard-absolute-layout){position:relative;display:block;width:100%;height:100%}\n"] }]
3763
3836
  }], ctorParameters: () => [], propDecorators: { dashboardElements: [{
3764
3837
  type: Input
3838
+ }], hasAbsoluteLayout: [{
3839
+ type: HostBinding,
3840
+ args: ['class.wuic-dashboard-absolute-layout']
3765
3841
  }] } });
3766
3842
 
3767
3843
  class CssSheetEditorComponent {
@@ -4527,79 +4603,12 @@ class DesignerComponent {
4527
4603
  this.metadataEditorSrv = metadataEditorSrv;
4528
4604
  this.chatbotHostRegistry = chatbotHostRegistry;
4529
4605
  this.workflowRuntimeMetadata = workflowRuntimeMetadata;
4530
- this.defaultCtxItems = [
4531
- {
4532
- label: this.t('designer.edit_properties', 'Edit properties'),
4533
- icon: 'pi pi-cog',
4534
- command: () => {
4535
- if (this.ctxElement) {
4536
- this.selectDashboardItem(this.ctxElement);
4537
- this.cd.detectChanges();
4538
- }
4539
- }
4540
- },
4541
- {
4542
- label: this.t('designer.remove', 'Remove'),
4543
- icon: 'pi pi-trash',
4544
- command: (menuItem) => {
4545
- if (this.ctxElement) {
4546
- this.removeElementByName(this.ctxElement.uniqueName);
4547
- }
4548
- }
4549
- }
4550
- ];
4606
+ // Context menu elemento: UNICA sorgente per tutti e tre i punti di accesso
4607
+ // (canvas / gerarchia / footer breadcrumb) -> `buildElementContextMenuItems`.
4608
+ // I tre handler `show*ContextMenu` ripopolano il rispettivo array con
4609
+ // l'elemento risolto al click, cosi' le voci sono sempre identiche.
4610
+ this.defaultCtxItems = this.buildElementContextMenuItems(null);
4551
4611
  this.ctxItems.next(this.defaultCtxItems);
4552
- this.footerCtxItems = [
4553
- {
4554
- label: this.t('designer.edit_properties', 'Edit properties'),
4555
- icon: 'pi pi-cog',
4556
- command: () => {
4557
- if (this.footerCtxElement) {
4558
- this.selectDashboardItem(this.footerCtxElement);
4559
- this.cd.detectChanges();
4560
- }
4561
- }
4562
- },
4563
- {
4564
- label: this.t('designer.rename', 'Rename'),
4565
- icon: 'pi pi-pencil',
4566
- command: () => {
4567
- this.renameFooterContextItem();
4568
- }
4569
- },
4570
- {
4571
- label: this.t('designer.remove', 'Remove'),
4572
- icon: 'pi pi-trash',
4573
- command: () => {
4574
- const uniqueName = String(this.footerCtxElement?.uniqueName || '').trim();
4575
- if (uniqueName) {
4576
- this.removeElementByName(uniqueName);
4577
- }
4578
- }
4579
- }
4580
- ];
4581
- this.hierarchyCtxItems = [
4582
- {
4583
- label: this.t('designer.edit_properties', 'Edit properties'),
4584
- icon: 'pi pi-cog',
4585
- command: () => {
4586
- if (this.hierarchyCtxElement) {
4587
- this.selectDashboardItem(this.hierarchyCtxElement);
4588
- this.cd.detectChanges();
4589
- }
4590
- }
4591
- },
4592
- {
4593
- label: this.t('designer.remove', 'Remove'),
4594
- icon: 'pi pi-trash',
4595
- command: () => {
4596
- const uniqueName = String(this.hierarchyCtxElement?.uniqueName || '').trim();
4597
- if (uniqueName) {
4598
- this.removeElementByName(uniqueName);
4599
- }
4600
- }
4601
- }
4602
- ];
4603
4612
  this.htmlStyleString = '[ngClass]="0.cssClass" [ngStyle]="{width: 0.width, maxWidth: 0.maxWidth, height: 0.height, maxHeight: 0.maxHeight, minWidth: 0.minWidth, minHeight: 0.minHeight, position: 0.position, left: 0.left, top: 0.top, float: 0.float, display: 0.display, boxSizing: 0.boxSizing, backgroundColor: 0.backgroundColor, color: 0.color, borderRadius: 0.borderRadius, borderBottomStyle: 0.borderBottomStyle, borderBottomWidth: 0.borderBottomWidth, borderBottomColor: 0.borderBottomColor, borderLeftStyle: 0.borderLeftStyle, borderLeftWidth: 0.borderLeftWidth, borderLeftColor: 0.borderLeftColor, borderRightStyle: 0.borderRightStyle, borderRightWidth: 0.borderRightWidth, borderRightColor: 0.borderRightColor, borderTopStyle: 0.borderTopStyle, borderTopWidth: 0.borderTopWidth, borderTopColor: 0.borderTopColor, paddingTop: 0.paddingTop, paddingRight: 0.paddingRight, paddingBottom: 0.paddingBottom, paddingLeft: 0.paddingLeft, marginTop: 0.marginTop, marginRight: 0.marginRight, marginBottom: 0.marginBottom, marginLeft: 0.marginLeft, fontSize: 0.fontSize, fontFamily: 0.fontFamily, fontWeight: 0.fontWeight, fontStyle: 0.fontStyle, textDecoration: 0.textDecoration, textAlign: 0.textAlign, verticalAlign: 0.verticalAlign, lineHeight: 0.lineHeight, letterSpacing: 0.letterSpacing, wordSpacing: 0.wordSpacing, textTransform: 0.textTransform, textIndent: 0.textIndent, whiteSpace: 0.whiteSpace, wordWrap: 0.wordWrap, wordBreak: 0.wordBreak, overflow: 0.overflow, overflowX: 0.overflowX, overflowY: 0.overflowY, zIndex: 0.zIndex, cursor: 0.cursor, visibility: 0.visibility, opacity: 0.opacity, filter: 0.filter, transform: 0.transform, transformOrigin: 0.transformOrigin, transition: 0.transition, boxShadow: 0.boxShadow, textShadow: 0.textShadow, backgroundPosition: 0.backgroundPosition, backgroundRepeat: 0.backgroundRepeat, backgroundAttachment: 0.backgroundAttachment, backgroundSize: 0.backgroundSize, backgroundImage: 0.backgroundImage, backgroundClip: 0.backgroundClip, backgroundOrigin: 0.backgroundOrigin, backgroundBlendMode: 0.backgroundBlendMode, clip: 0.clip, clipPath: 0.clipPath, clipRule: 0.clipRule, mask: 0.mask, maskType: 0.maskType, maskImage: 0.maskImage, maskMode: 0.maskMode, maskSize: 0.maskSize, maskRepeat: 0.maskRepeat, maskPosition: 0.maskPosition, maskClip: 0.maskClip, maskOrigin: 0.maskOrigin, maskComposite: 0.maskComposite, maskBorder: 0.maskBorder, maskBorderSource: 0.maskBorderSource, maskBorderSlice: 0.maskBorderSlice, maskBorderWidth: 0.maskBorderWidth, maskBorderOutset: 0.maskBorderOutset, maskBorderRepeat: 0.maskBorderRepeat, maskBorderMode: 0.maskBorderMode, maskBorderClip: 0.maskBorderClip, maskBorderOrigin: 0.maskBorderOrigin, maskBorder, tableLayout: 0.tableLayout, borderCollapse: 0.borderCollapse, borderSpacing: 0.borderSpacing, captionSide: 0.captionSide, emptyCells: 0.emptyCells, tableCaption: 0.tableCaption, tableBorder: 0.tableBorder, tableBorderSpacing: 0.tableBorderSpacing, tableBorderCollapse: 0.tableBorderCollapse, listStyle: 0.listStyle}"';
4604
4613
  this.htmlInputProperties = {
4605
4614
  animation: { type: 'string' },
@@ -5108,6 +5117,27 @@ class DesignerComponent {
5108
5117
  height: '280px'
5109
5118
  })
5110
5119
  },
5120
+ {
5121
+ group: 'HTML',
5122
+ toolId: 130,
5123
+ name: 'IMAGE',
5124
+ tag: `<wuic-image-wrapper-lazy [attr.id]="uniqueName" [src]="inputs.src" [alt]="inputs.alt" [preview]="inputs.preview" [previewImageSrc]="inputs.previewImageSrc" [databound]="inputs.datasource?.component" [bindingFunction]="inputs.bindingFunction" [inputs]="inputs" ngxDraggableDom="true" ngResizable [rzHandles] = "'se'" (rzResizing)="onResizing($event, this)" (moved)="onMoving($event, this)" ${this.htmlStyleString.replace(/0/g, 'inputs')}></wuic-image-wrapper-lazy>`,
5125
+ icon: 'pi pi-images',
5126
+ inputProps: Object.assign({}, this.htmlInputProperties, {
5127
+ src: { type: 'string', propertyCaption: 'Source URL' },
5128
+ previewImageSrc: { type: 'string', propertyCaption: 'Preview (full-size) URL' },
5129
+ alt: { type: 'string', propertyCaption: 'Alt text' },
5130
+ preview: { type: 'boolean', propertyCaption: 'Enable lightbox preview' }
5131
+ }),
5132
+ inputs: Object.assign({}, this.htmlInputs, {
5133
+ src: '',
5134
+ previewImageSrc: '',
5135
+ alt: '',
5136
+ preview: true,
5137
+ width: '240px',
5138
+ height: '160px'
5139
+ })
5140
+ },
5111
5141
  {
5112
5142
  group: 'HTML',
5113
5143
  toolId: 7,
@@ -5476,7 +5506,10 @@ class DesignerComponent {
5476
5506
  selected: { type: 'boolean', propertyCaption: 'Selected', hide: true }
5477
5507
  },
5478
5508
  inputs: { header: '<header>' },
5479
- nestedComponents: []
5509
+ nestedComponents: [],
5510
+ // Appena droppato dentro un TABVIEW il tab deve diventare quello attivo,
5511
+ // altrimenti la sua area non e' visibile e non ci si possono droppare figli.
5512
+ onDrop: this.dropTabPanel.bind(this)
5480
5513
  },
5481
5514
  {
5482
5515
  group: 'CONTAINER',
@@ -5722,7 +5755,18 @@ class DesignerComponent {
5722
5755
  // Aggiorna il last top-level per i prossimi placeholder. NB: usiamo
5723
5756
  // findLastInjectedNode per pescare l'istanza vera (con onDrop appena
5724
5757
  // eseguito = TR/TD/AREA gia' popolati), non il template clonato.
5725
- lastTopLevelInjected = this.findLastInjectedNode(String(node.tool_name || ''), topParent) || lastTopLevelInjected;
5758
+ // FIX 2026-06-21 (designer-canvas-verify [2], layout 3-colonne): aggiorna
5759
+ // SOLO se il nodo iniettato e' un CONTAINER che auto-crea celle/aree
5760
+ // referenziabili via placeholder (TABLE/SPLITTER/ACCORDION/TABVIEW). I
5761
+ // leaf (DATASOURCE/DATAREPEATER/LABEL/...) NON devono sostituirlo: altrimenti
5762
+ // i placeholder <TD-N>/<AREA-N> dei nodi SUCCESSIVI risolverebbero contro
5763
+ // il leaf invece del container -> errore "Placeholder '<TD-0>' non
5764
+ // risolvibile dentro DATASOURCE__6" e i DATAREPEATER restano 0/3. Cosi'
5765
+ // tutti i nodi di un layout multi-cella risolvono contro lo stesso container.
5766
+ const PLACEHOLDER_CONTAINERS = ['TABLE', 'SPLITTER', 'ACCORDION', 'TABVIEW'];
5767
+ if (PLACEHOLDER_CONTAINERS.indexOf(String(node.tool_name || '').toUpperCase()) >= 0) {
5768
+ lastTopLevelInjected = this.findLastInjectedNode(String(node.tool_name || ''), topParent) || lastTopLevelInjected;
5769
+ }
5726
5770
  }
5727
5771
  const rationale = (action?.['rationale'] || '').toString().slice(0, 100);
5728
5772
  return { ok: true, message: `Iniettati ${totalInjected} elementi nel designer${rationale ? ': ' + rationale : ''}.` };
@@ -6092,6 +6136,48 @@ class DesignerComponent {
6092
6136
  *
6093
6137
  * Ritorna il numero di elementi creati (1 + somma dei children).
6094
6138
  */
6139
+ /**
6140
+ * Inietta la config di archetipo (`md_props_bag.archetypes.<action>`) nel
6141
+ * `metaInfo.tableMetadata` del DATASOURCE bindato a un DATAREPEATER appena
6142
+ * droppato. NESSUNA chiamata server: muta solo lo stato in-memory del
6143
+ * datasource (md_props_bag + extraProps.archetypes) e ri-emette `fetchInfo$`
6144
+ * cosi' il DATAREPEATER reagisce e renderizza l'archetipo configurato. La
6145
+ * config viene poi serializzata insieme al datasource quando l'utente salva
6146
+ * la dashboard (boardcontent), coerente con AGENTS regola 7 (md_props_bag e'
6147
+ * la fonte di verita', extraProps e' la proiezione runtime).
6148
+ * @param dsBinding il binding gia' idratato (`{uniqueName, component: BehaviorSubject<DataSourceComponent>}`)
6149
+ * @param action l'archetipo (chart/map/scheduler/kanban/tree/carousel/spreadsheet/...)
6150
+ * @param config oggetto config = contenuto di archetypes.<action>
6151
+ */
6152
+ /**
6153
+ * Applica `archetype_config` (dal nodo designer_inject) a un DATAREPEATER tool
6154
+ * APPENA droppato, posticipando finche' il DataSourceComponent bindato e'
6155
+ * materializzato (`fetchData` disponibile). Poi delega ad
6156
+ * {@link applyArchetypeConfigToTool} — il path proven del configuratore.
6157
+ */
6158
+ deferApplyArchetypeConfig(tool, action, config) {
6159
+ if (!tool || !action || !config || typeof config !== 'object')
6160
+ return;
6161
+ let tries = 0;
6162
+ const attempt = () => {
6163
+ const ds = tool.inputs?.['datasource']?.component?.value;
6164
+ if (ds && typeof ds.fetchData === 'function') {
6165
+ try {
6166
+ this.applyArchetypeConfigToTool(tool, action, config);
6167
+ }
6168
+ catch (e) {
6169
+ try {
6170
+ console.warn('[deferApplyArchetypeConfig] apply failed', e);
6171
+ }
6172
+ catch { /* */ }
6173
+ }
6174
+ return;
6175
+ }
6176
+ if (tries++ < 60)
6177
+ setTimeout(attempt, 200); // ~12s max per la materializzazione del ds
6178
+ };
6179
+ attempt();
6180
+ }
6095
6181
  async injectLayoutNode(node, parentUniqueName) {
6096
6182
  const toolName = String(node?.tool_name || '').trim();
6097
6183
  if (!toolName)
@@ -6365,6 +6451,25 @@ class DesignerComponent {
6365
6451
  }
6366
6452
  catch { /* ignore */ }
6367
6453
  }
6454
+ // CONFIG ARCHETIPO INLINE: il nodo puo' portare `archetype_config`
6455
+ // (= md_props_bag.archetypes.<action> JSON, es. chart {type,options,
6456
+ // dataOptions}, map {zoom,center,...}, scheduler {fromField,...},
6457
+ // kanban {statusField,statusColumns,...}). NESSUNA chiamata server: la
6458
+ // config viene scritta nel metaInfo.tableMetadata del DATASOURCE bindato
6459
+ // (md_props_bag + extraProps.archetypes); il DATAREPEATER reagisce via
6460
+ // fetchInfo$ e renderizza l'archetipo configurato. Persiste alla
6461
+ // serializzazione del datasource quando si salva la dashboard.
6462
+ const archCfg = (node && (node.archetype_config || node.archetypeConfig))
6463
+ || (node?.inputs && (node.inputs.archetype_config || node.inputs.archetypeConfig))
6464
+ || null;
6465
+ if (archCfg && typeof archCfg === 'object') {
6466
+ // Defer: il DataSourceComponent bindato (e l'archetipo renderizzato) si
6467
+ // materializzano async dopo il drop. Aspetta che il ds sia pronto
6468
+ // (fetchData disponibile), poi applica via il path PROVEN del configuratore
6469
+ // (`applyArchetypeConfigToTool`: customProps_<action> + metaInfo + ds.fetchData
6470
+ // + propertyTreeBuilder) — lo STESSO usato da "Configura chart".
6471
+ this.deferApplyArchetypeConfig(justInjected, String(action), archCfg);
6472
+ }
6368
6473
  }
6369
6474
  }
6370
6475
  let count = 1;
@@ -7393,11 +7498,53 @@ class DesignerComponent {
7393
7498
  if (this.isTypingTarget(event.target)) {
7394
7499
  return;
7395
7500
  }
7501
+ const rawKey = String(event.key || '');
7396
7502
  const ctrlOrMeta = !!event.ctrlKey || !!event.metaKey;
7503
+ // ---- Scorciatoie SENZA modificatore (feature B) ----
7397
7504
  if (!ctrlOrMeta) {
7505
+ // Delete/Backspace: rimuove l'elemento selezionato (o TUTTI i multi-selezionati).
7506
+ if (rawKey === 'Delete' || rawKey === 'Backspace') {
7507
+ if (this.selectedTools.length > 1) {
7508
+ event.preventDefault();
7509
+ const names = this.selectedTools.map((t) => t.uniqueName);
7510
+ this.clearMultiSelection();
7511
+ names.forEach((n) => this.removeElementByName(n));
7512
+ this.commitHistoryIfChanged();
7513
+ return;
7514
+ }
7515
+ if (this.tool?.uniqueName) {
7516
+ event.preventDefault();
7517
+ this.removeElementByName(this.tool.uniqueName);
7518
+ this.commitHistoryIfChanged();
7519
+ return;
7520
+ }
7521
+ }
7522
+ // Frecce: nudge di posizione dell'elemento selezionato (passo = gridSize se
7523
+ // snap attivo, altrimenti 1px; con Shift un passo piu' grande).
7524
+ const nudges = {
7525
+ ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1]
7526
+ };
7527
+ if (nudges[rawKey] && (this.tool || this.selectedTools.length > 1)) {
7528
+ event.preventDefault();
7529
+ const step = (this.snapToGridEnabled ? this.gridSize : 1) * (event.shiftKey ? 5 : 1);
7530
+ const ux = nudges[rawKey][0] * step, uy = nudges[rawKey][1] * step;
7531
+ if (this.selectedTools.length > 1) {
7532
+ this.nudgeElements(this.selectedTools, ux, uy);
7533
+ }
7534
+ else {
7535
+ this.nudgeSelectedElement(ux, uy);
7536
+ }
7537
+ return;
7538
+ }
7539
+ return;
7540
+ }
7541
+ const key = rawKey.toLowerCase();
7542
+ // Ctrl/Cmd+S: salva (previene il dialog di salvataggio del browser).
7543
+ if (key === 's') {
7544
+ event.preventDefault();
7545
+ void this.saveDashboard();
7398
7546
  return;
7399
7547
  }
7400
- const key = String(event.key || '').toLowerCase();
7401
7548
  if (key === 'z' && event.shiftKey) {
7402
7549
  event.preventDefault();
7403
7550
  this.redo();
@@ -7411,6 +7558,28 @@ class DesignerComponent {
7411
7558
  if (key === 'y') {
7412
7559
  event.preventDefault();
7413
7560
  this.redo();
7561
+ return;
7562
+ }
7563
+ if (key === 'd') {
7564
+ event.preventDefault();
7565
+ void this.duplicateDesignerElement(this.tool);
7566
+ return;
7567
+ }
7568
+ if (key === 'c') {
7569
+ // Non intercettare Ctrl+C se l'utente sta copiando una selezione di testo.
7570
+ const textSelection = (typeof window !== 'undefined' && window.getSelection)
7571
+ ? String(window.getSelection() || '')
7572
+ : '';
7573
+ if (textSelection) {
7574
+ return;
7575
+ }
7576
+ event.preventDefault();
7577
+ this.copyDesignerElement(this.tool);
7578
+ return;
7579
+ }
7580
+ if (key === 'v') {
7581
+ event.preventDefault();
7582
+ void this.pasteDesignerElement();
7414
7583
  }
7415
7584
  }
7416
7585
  /**
@@ -7636,12 +7805,49 @@ class DesignerComponent {
7636
7805
  {
7637
7806
  separator: true
7638
7807
  },
7808
+ {
7809
+ label: this.t('designer.menu_export_json', 'Esporta JSON'),
7810
+ icon: 'pi pi-download',
7811
+ command: () => this.exportDashboardJson()
7812
+ },
7813
+ {
7814
+ label: this.t('designer.menu_import_json', 'Importa JSON'),
7815
+ icon: 'pi pi-upload',
7816
+ command: () => this.importDashboardJson()
7817
+ },
7818
+ {
7819
+ label: this.t('designer.menu_save_preset', 'Salva come preset'),
7820
+ icon: 'pi pi-bookmark',
7821
+ command: () => this.saveDashboardAsPreset()
7822
+ },
7823
+ {
7824
+ label: this.t('designer.menu_apply_preset', 'Applica preset'),
7825
+ icon: 'pi pi-bookmark-fill',
7826
+ command: () => this.applyDashboardPreset()
7827
+ },
7828
+ {
7829
+ separator: true
7830
+ },
7639
7831
  {
7640
7832
  label: this.hideDatasourcesInCanvas
7641
7833
  ? this.t('designer.menu_show_datasources', 'Mostra datasource nel canvas')
7642
7834
  : this.t('designer.menu_hide_datasources', 'Nascondi datasource nel canvas'),
7643
7835
  icon: this.hideDatasourcesInCanvas ? 'pi pi-eye' : 'pi pi-eye-slash',
7644
7836
  command: () => this.toggleHideDatasourcesInCanvas()
7837
+ },
7838
+ {
7839
+ label: this.snapToGridEnabled
7840
+ ? this.t('designer.menu_snap_off', 'Disattiva snap alla griglia')
7841
+ : this.t('designer.menu_snap_on', 'Attiva snap alla griglia'),
7842
+ icon: this.snapToGridEnabled ? 'pi pi-th-large' : 'pi pi-table',
7843
+ command: () => this.toggleSnapToGrid()
7844
+ },
7845
+ {
7846
+ label: this.absoluteFlowEnabled
7847
+ ? this.t('designer.menu_absflow_off', 'Disattiva flusso assoluto')
7848
+ : this.t('designer.menu_absflow_on', 'Attiva flusso assoluto'),
7849
+ icon: 'pi pi-arrows-alt',
7850
+ command: () => this.toggleAbsoluteFlow()
7645
7851
  }
7646
7852
  ];
7647
7853
  }
@@ -7649,99 +7855,1151 @@ class DesignerComponent {
7649
7855
  this.hideDatasourcesInCanvas = !this.hideDatasourcesInCanvas;
7650
7856
  this.refreshGraphActionsMenuItems();
7651
7857
  }
7652
- get droppedDashboardItems() {
7653
- return (this.flattenedDashboardElements || []).filter((item) => !!item?.uniqueName);
7654
- }
7655
- get hierarchyTreeNodes() {
7656
- return this.hierarchyTreeNodesState;
7858
+ /**
7859
+ * Flusso "assoluto" (default OFF = flusso normale come sempre). Quando ON, ogni
7860
+ * elemento droppato nasce `position: absolute` con `left/top` dalla posizione
7861
+ * del drop -> non partecipa al flusso, quindi ridimensionarne uno NON sposta gli
7862
+ * altri. E' per-elemento (chi nasce con position:absolute segue il nuovo modello,
7863
+ * gli elementi salvati senza restano a flusso) -> retrocompatibile senza flag globale.
7864
+ */
7865
+ absoluteFlowEnabled = false;
7866
+ toggleAbsoluteFlow() {
7867
+ const turningOn = !this.absoluteFlowEnabled;
7868
+ // Se attivo il flusso assoluto con elementi gia' nel canvas, quelli restano a
7869
+ // flusso: notifico che vanno riposizionati manualmente.
7870
+ if (turningOn && (this.dashboardElements?.length || 0) > 0) {
7871
+ WtoolboxService.messageNotificationService.add({
7872
+ severity: 'warn',
7873
+ summary: this.t('designer.absflow.title', 'Flusso assoluto'),
7874
+ detail: this.t('designer.absflow.existing_manual', 'Gli elementi gia\' presenti restano nel flusso attuale: vanno riposizionati a mano (position/left/top) per adottare il layout assoluto.')
7875
+ });
7876
+ }
7877
+ this.absoluteFlowEnabled = turningOn;
7878
+ this.refreshGraphActionsMenuItems();
7879
+ this.cd.detectChanges();
7657
7880
  }
7658
- get canUndo() {
7659
- return this.undoHistory.length > 0;
7881
+ /**
7882
+ * Posizione del drop relativa a un elemento di riferimento (il canvas per i drop
7883
+ * top-level, oppure il container quando droppo dentro un contenitore), snappata
7884
+ * alla griglia se lo snap e' attivo.
7885
+ */
7886
+ getDropPositionRelativeTo(event, refEl) {
7887
+ const rect = refEl ? refEl.getBoundingClientRect() : null;
7888
+ const oe = (event && event.originalEvent) ? event.originalEvent : event;
7889
+ const cx = (typeof oe?.clientX === 'number') ? oe.clientX : (typeof event?.clientX === 'number' ? event.clientX : null);
7890
+ const cy = (typeof oe?.clientY === 'number') ? oe.clientY : (typeof event?.clientY === 'number' ? event.clientY : null);
7891
+ if (!rect || cx === null || cy === null) {
7892
+ return { left: 0, top: 0 };
7893
+ }
7894
+ // Coordinate del cursore relative al box del riferimento (client rect gia'
7895
+ // include l'eventuale bordo/scroll del riferimento: sottraggo il suo scroll).
7896
+ let left = cx - rect.left + (refEl ? refEl.scrollLeft : 0);
7897
+ let top = cy - rect.top + (refEl ? refEl.scrollTop : 0);
7898
+ const g = DesignerSnapState.gridSize;
7899
+ if (g && g > 0) {
7900
+ left = Math.round(left / g) * g;
7901
+ top = Math.round(top / g) * g;
7902
+ }
7903
+ return { left: Math.max(0, Math.round(left)), top: Math.max(0, Math.round(top)) };
7904
+ }
7905
+ /** Snap alla griglia per drag/resize (feature B). Off di default. */
7906
+ snapToGridEnabled = false;
7907
+ /** Passo della griglia di snap in px. */
7908
+ gridSize = 10;
7909
+ /**
7910
+ * Attiva/disattiva lo snap alla griglia: propaga la dimensione griglia alle
7911
+ * istanze DynamicDashboardTemplateComponent (che arrotondano posizione/dimensione
7912
+ * in onMoving/onResizing) e mostra un overlay griglia sul canvas.
7913
+ */
7914
+ toggleSnapToGrid() {
7915
+ this.snapToGridEnabled = !this.snapToGridEnabled;
7916
+ DynamicDashboardTemplateComponent.snapGridSize = this.snapToGridEnabled ? this.gridSize : 0;
7917
+ // Alimenta lo stato condiviso letto dalle direttive drag/resize (lo snap
7918
+ // effettivo su move/resize avviene li', non solo nel bookkeeping onMoving).
7919
+ DesignerSnapState.gridSize = this.snapToGridEnabled ? this.gridSize : 0;
7920
+ // Attivando lo snap, allinea SUBITO alla griglia anche gli elementi gia'
7921
+ // presenti (droppati/spostati prima di attivarlo).
7922
+ if (this.snapToGridEnabled) {
7923
+ this.snapAllExistingElementsToGrid();
7924
+ }
7925
+ this.refreshGraphActionsMenuItems();
7926
+ this.cd.detectChanges();
7660
7927
  }
7661
- get canRedo() {
7662
- return this.redoHistory.length > 0;
7928
+ /** Snappa alla griglia la posizione assoluta di tutti gli elementi top-level presenti. */
7929
+ snapAllExistingElementsToGrid() {
7930
+ const g = DesignerSnapState.gridSize;
7931
+ if (!g || g <= 0) {
7932
+ return;
7933
+ }
7934
+ setTimeout(() => {
7935
+ let changed = false;
7936
+ for (const tool of (this.dashboardElements || [])) {
7937
+ if (this.snapElementAbsoluteToGrid(tool, g)) {
7938
+ changed = true;
7939
+ }
7940
+ }
7941
+ if (changed) {
7942
+ this.commitHistoryIfChanged();
7943
+ this.cd.detectChanges();
7944
+ }
7945
+ }, 0);
7663
7946
  }
7664
7947
  /**
7665
- * Calcola la label visualizzata per un elemento nel pannello "dropped items".
7948
+ * Allinea la posizione VISIVA assoluta di un elemento al multiplo di griglia
7949
+ * agendo sulla traslazione del suo `transform` (offsetLeft/Top escludono il
7950
+ * transform -> assoluto = offset + traslazione). Ritorna true se ha modificato.
7666
7951
  */
7667
- getDroppedDashboardItemLabel(item) {
7668
- const displayName = String(item?.displayName || '').trim();
7669
- if (displayName) {
7670
- return displayName;
7952
+ snapElementAbsoluteToGrid(tool, g) {
7953
+ const dom = tool?.uniqueName ? document.getElementById(tool.uniqueName) : null;
7954
+ if (!dom || !tool.inputs) {
7955
+ return false;
7671
7956
  }
7672
- const uniqueName = String(item?.uniqueName || '').trim();
7673
- const name = String(item?.name || '').trim();
7674
- if (!uniqueName) {
7675
- return name;
7957
+ let sx = 1, sy = 0, kx = 0, ky = 1, tx = 0, ty = 0;
7958
+ const m = String(tool.inputs['transform'] || '').match(/matrix\(([^)]+)\)/);
7959
+ if (m) {
7960
+ const p = m[1].split(',').map((x) => parseFloat(x));
7961
+ if (p.length >= 6 && p.every((n) => !isNaN(n))) {
7962
+ [sx, sy, kx, ky, tx, ty] = p;
7963
+ }
7676
7964
  }
7677
- return name ? `${uniqueName} (${name})` : uniqueName;
7965
+ const newTx = Math.round((dom.offsetLeft + tx) / g) * g - dom.offsetLeft;
7966
+ const newTy = Math.round((dom.offsetTop + ty) / g) * g - dom.offsetTop;
7967
+ if (newTx === tx && newTy === ty) {
7968
+ return false;
7969
+ }
7970
+ tool.inputs['transform'] = `matrix(${sx}, ${sy}, ${kx}, ${ky}, ${newTx}, ${newTy})`;
7971
+ return true;
7678
7972
  }
7679
7973
  /**
7680
- * Imposta come selezionato l'elemento cliccato nella lista elementi.
7974
+ * Sposta l'elemento selezionato di (dx, dy) px agendo sulla translation della
7975
+ * matrice `transform` (stesso campo che usa onMoving). Usato dalle frecce.
7681
7976
  */
7682
- selectDashboardItem(item) {
7683
- if (!item) {
7977
+ nudgeSelectedElement(dx, dy) {
7978
+ const el = this.tool;
7979
+ if (!el?.inputs) {
7684
7980
  return;
7685
7981
  }
7686
- this.tool = item;
7687
- this.syncHierarchySelectionToCurrentTool();
7688
- }
7689
- showHierarchyNodeContextMenu(event, node, menu) {
7690
- event.preventDefault();
7691
- event.stopPropagation();
7692
- const uniqueName = String(node?.key || '').trim();
7693
- this.hierarchyCtxElement = uniqueName ? this.getElementByName(uniqueName) : null;
7694
- menu.show(event);
7982
+ let scaleX = 1, skewY = 0, skewX = 0, scaleY = 1, tx = 0, ty = 0;
7983
+ const cur = String(el.inputs['transform'] || '');
7984
+ const m = cur.match(/matrix\(([^)]+)\)/);
7985
+ if (m) {
7986
+ const parts = m[1].split(',').map((x) => parseFloat(x));
7987
+ if (parts.length >= 6 && parts.every((n) => !isNaN(n))) {
7988
+ [scaleX, skewY, skewX, scaleY, tx, ty] = parts;
7989
+ }
7990
+ }
7991
+ el.inputs['transform'] = `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${tx + dx}, ${ty + dy})`;
7992
+ this.commitHistoryIfChanged();
7993
+ this.cd.detectChanges();
7695
7994
  }
7696
- onHierarchyNodeSelect(event) {
7697
- const node = event?.node;
7698
- if (!node) {
7699
- return;
7995
+ // ===================== Feature F: preset/template riusabili =====================
7996
+ // Libreria di preset lato client (localStorage): mappa nome -> boardcontent JSON.
7997
+ // Riusa buildSerializedDashboardElements (salva) e loadDashboard (applica), gli
7998
+ // stessi choke-point di save/import. Nessuno schema change.
7999
+ static PRESETS_STORAGE_KEY = 'wuic.designer.presets';
8000
+ readDesignerPresets() {
8001
+ try {
8002
+ const raw = localStorage.getItem(DesignerComponent.PRESETS_STORAGE_KEY);
8003
+ const parsed = raw ? JSON.parse(raw) : {};
8004
+ return (parsed && typeof parsed === 'object') ? parsed : {};
7700
8005
  }
7701
- const uniqueName = String(node?.key || '').trim();
7702
- if (!uniqueName) {
7703
- return;
8006
+ catch {
8007
+ return {};
7704
8008
  }
7705
- const item = this.getElementByName(uniqueName);
7706
- if (!item) {
7707
- return;
8009
+ }
8010
+ writeDesignerPresets(presets) {
8011
+ try {
8012
+ localStorage.setItem(DesignerComponent.PRESETS_STORAGE_KEY, JSON.stringify(presets));
7708
8013
  }
7709
- this.selectDashboardItem(item);
7710
- this.cd.detectChanges();
8014
+ catch { /* quota / storage disabilitato */ }
7711
8015
  }
7712
- onHierarchyNodeMouseEnter(node) {
7713
- const uniqueName = String(node?.key || '').trim();
7714
- if (!uniqueName) {
8016
+ /** Salva il layout corrente come preset nominato nella libreria locale. */
8017
+ async saveDashboardAsPreset() {
8018
+ const resp = await WtoolboxService.promptDialog(this.t('designer.preset.save_title', 'Salva come preset'), [{ name: 'presetName', caption: this.t('designer.preset.name_caption', 'Nome preset'), type: 'string', value: '', required: true }], null, '400px', '230px');
8019
+ const name = String(resp?.presetName?.value || '').trim();
8020
+ if (!name) {
7715
8021
  return;
7716
8022
  }
7717
- this.setHierarchyHoverHighlightByKey(uniqueName);
7718
- }
7719
- onHierarchyNodeMouseLeave() {
7720
- this.clearHierarchyHoverHighlight();
7721
- }
7722
- isHierarchyNodeSelected(node) {
7723
- const nodeKey = String(node?.key || '').trim();
7724
- const selectedKey = String(this.hierarchySelection?.key || '').trim();
7725
- return !!nodeKey && nodeKey === selectedKey;
8023
+ const presets = this.readDesignerPresets();
8024
+ presets[name] = this.buildSerializedDashboardElements();
8025
+ this.writeDesignerPresets(presets);
8026
+ WtoolboxService.messageNotificationService.add({
8027
+ severity: 'success',
8028
+ summary: this.t('success', 'Success'),
8029
+ detail: this.t('designer.preset.saved', 'Preset salvato')
8030
+ });
7726
8031
  }
7727
- setHierarchyHoverHighlightByKey(uniqueName) {
7728
- const key = String(uniqueName || '').trim();
7729
- this.clearHierarchyHoverHighlight();
7730
- if (!key) {
8032
+ /** Applica un preset della libreria al canvas (sostituisce il layout corrente). */
8033
+ async applyDashboardPreset() {
8034
+ const presets = this.readDesignerPresets();
8035
+ const names = Object.keys(presets);
8036
+ if (names.length === 0) {
8037
+ WtoolboxService.messageNotificationService.add({
8038
+ severity: 'info',
8039
+ summary: this.t('info', 'Info'),
8040
+ detail: this.t('designer.preset.none', 'Nessun preset salvato')
8041
+ });
7731
8042
  return;
7732
8043
  }
7733
- const item = this.getElementByName(key);
7734
- if (!item) {
8044
+ const resp = await WtoolboxService.promptDialog(this.t('designer.preset.apply_title', 'Applica preset'), [{
8045
+ name: 'preset',
8046
+ caption: this.t('designer.preset.pick_caption', 'Preset'),
8047
+ type: 'dictionary',
8048
+ required: true,
8049
+ dictionaryData: names.map((n) => ({ label: n, value: n }))
8050
+ }], null, '400px', '230px');
8051
+ const chosen = String(resp?.preset?.value || '').trim();
8052
+ if (!chosen || !presets[chosen]) {
7735
8053
  return;
7736
8054
  }
7737
- this.setFooterHoverHighlight(item);
7738
- if (typeof document === 'undefined') {
7739
- return;
8055
+ if ((this.flattenedDashboardElements || []).length > 0) {
8056
+ const ok = await WtoolboxService.confirm({
8057
+ header: this.t('warning', 'Warning'),
8058
+ message: this.t('designer.preset.apply_confirm', 'Applicare il preset sostituira\' il canvas corrente. Continuare?'),
8059
+ acceptLabel: this.t('confirm', 'Confirm'),
8060
+ rejectLabel: this.t('cancel', 'Cancel')
8061
+ });
8062
+ if (!ok) {
8063
+ return;
8064
+ }
7740
8065
  }
7741
- const footerElement = document.querySelector(`.designer-footer-item[data-designer-unique-name="${key}"]`);
7742
- if (footerElement) {
7743
- footerElement.classList.add('designer-tree-hover-target');
7744
- this.hierarchyHoverFooterElement = footerElement;
8066
+ try {
8067
+ await this.loadDashboard({
8068
+ boardcontent: presets[chosen],
8069
+ board_route: this.currentDashboardRoute || '',
8070
+ board_des: this.currentDashboardDescription || ''
8071
+ }, true);
8072
+ WtoolboxService.messageNotificationService.add({
8073
+ severity: 'success',
8074
+ summary: this.t('success', 'Success'),
8075
+ detail: this.t('designer.preset.applied', 'Preset applicato')
8076
+ });
8077
+ }
8078
+ catch (error) {
8079
+ console.error('Failed to apply designer preset', error);
8080
+ WtoolboxService.errorHandler?.handleError?.(error);
8081
+ }
8082
+ }
8083
+ /** Clipboard interno per copy/paste di elementi del designer (inject-node). */
8084
+ designerClipboard = null;
8085
+ /**
8086
+ * Converte un elemento del canvas nella shape `node` accettata da
8087
+ * {@link injectLayoutNode} (`{ tool_name, inputs }`), estraendo i soli input
8088
+ * SCALARI (string/number/boolean/array) + i binding datasource (ridotti al
8089
+ * loro `uniqueName`, che il path canonico ri-idrata post-drop). Function e
8090
+ * oggetti complessi vengono scartati: verranno ricostruiti dal template della
8091
+ * palette al drop. Ritorna null se l'elemento non e' copiabile.
8092
+ */
8093
+ buildInjectNodeFromElement(el) {
8094
+ const name = String(el?.name || '').trim();
8095
+ if (!name) {
8096
+ return null;
8097
+ }
8098
+ const inputs = {};
8099
+ const rawInputs = el?.inputs || {};
8100
+ for (const key of Object.keys(rawInputs)) {
8101
+ let v = rawInputs[key];
8102
+ if (v instanceof BehaviorSubject) {
8103
+ v = v.value;
8104
+ }
8105
+ if (typeof v === 'function') {
8106
+ continue;
8107
+ }
8108
+ // Binding datasource (dropped-component): passa il solo uniqueName stringa
8109
+ if (v && typeof v === 'object' && !Array.isArray(v) && 'uniqueName' in v && 'component' in v) {
8110
+ const un = String(v.uniqueName || '').trim();
8111
+ if (un) {
8112
+ inputs[key] = un;
8113
+ }
8114
+ continue;
8115
+ }
8116
+ // Scarta gli altri oggetti complessi non serializzabili come scalari
8117
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
8118
+ continue;
8119
+ }
8120
+ inputs[key] = v;
8121
+ }
8122
+ return { tool_name: name, inputs };
8123
+ }
8124
+ /** Trova l'uniqueName del parent di un elemento nel tree, o null se e' a root. */
8125
+ findParentUniqueName(target) {
8126
+ const targetUn = String(target?.uniqueName || '').trim();
8127
+ if (!targetUn) {
8128
+ return null;
8129
+ }
8130
+ const walk = (nodes) => {
8131
+ for (const n of nodes || []) {
8132
+ const kids = n?.nestedComponents || [];
8133
+ if (kids.some((k) => String(k?.uniqueName || '') === targetUn)) {
8134
+ return String(n?.uniqueName || '') || null;
8135
+ }
8136
+ const deeper = walk(kids);
8137
+ if (deeper !== undefined) {
8138
+ return deeper;
8139
+ }
8140
+ }
8141
+ return undefined;
8142
+ };
8143
+ const res = walk(this.dashboardElements || []);
8144
+ return res === undefined ? null : res;
8145
+ }
8146
+ /**
8147
+ * Copia l'elemento selezionato (o `ctxElement`) nel clipboard interno.
8148
+ * I contenitori con figli non sono ancora supportati (richiedono il mapping
8149
+ * dei placeholder strutturali): mostra un avviso e non copia.
8150
+ */
8151
+ copyDesignerElement(el) {
8152
+ const target = el || this.tool || this.ctxElement;
8153
+ if (!target) {
8154
+ return;
8155
+ }
8156
+ if (Array.isArray(target.nestedComponents) && target.nestedComponents.length > 0) {
8157
+ WtoolboxService.messageNotificationService.add({
8158
+ severity: 'info',
8159
+ summary: this.t('info', 'Info'),
8160
+ detail: this.t('designer.clipboard.container_unsupported', 'La copia di contenitori con figli non e\' ancora supportata')
8161
+ });
8162
+ return;
8163
+ }
8164
+ // Gli elementi con binding datasource "live" + rendering per-riga
8165
+ // (DATAREPEATER/SELECT/MULTISELECT/UL) non si duplicano ancora in modo
8166
+ // affidabile: il clone via injectLayoutNode ricrea un list-grid il cui
8167
+ // selection model non e' inizializzato ad array -> IsSelectedRowPipe fallisce
8168
+ // (`items.find is not a function`). Fino a quando la re-idratazione del
8169
+ // binding non e' gestita, escludiamo questi tipi con un avviso chiaro.
8170
+ const DATASOURCE_BOUND = new Set(['DATAREPEATER', 'SELECT', 'MULTISELECT', 'UL']);
8171
+ if (DATASOURCE_BOUND.has(String(target.name || ''))) {
8172
+ WtoolboxService.messageNotificationService.add({
8173
+ severity: 'info',
8174
+ summary: this.t('info', 'Info'),
8175
+ detail: this.t('designer.clipboard.datasource_bound_unsupported', 'La duplicazione di componenti legati a un datasource non e\' ancora supportata')
8176
+ });
8177
+ return;
8178
+ }
8179
+ const node = this.buildInjectNodeFromElement(target);
8180
+ if (!node) {
8181
+ return;
8182
+ }
8183
+ this.designerClipboard = node;
8184
+ WtoolboxService.messageNotificationService.add({
8185
+ severity: 'success',
8186
+ summary: this.t('success', 'Success'),
8187
+ detail: this.t('designer.clipboard.copied', 'Elemento copiato')
8188
+ });
8189
+ }
8190
+ /**
8191
+ * Incolla l'elemento dal clipboard. Se `parentUniqueName` e' passato (usato da
8192
+ * `duplicateDesignerElement` per creare un sibling) inietta li'; altrimenti
8193
+ * dentro l'elemento selezionato se e' un contenitore, altrimenti a root.
8194
+ * Riusa il path canonico {@link injectLayoutNode} (drop() reale) per garantire
8195
+ * uniqueName freschi, hydration e history coerenti.
8196
+ */
8197
+ async pasteDesignerElement(parentUniqueName) {
8198
+ if (!this.designerClipboard) {
8199
+ return;
8200
+ }
8201
+ let parent = parentUniqueName ?? null;
8202
+ if (parent === null && this.tool && Array.isArray(this.tool.nestedComponents)) {
8203
+ parent = String(this.tool.uniqueName || '') || null;
8204
+ }
8205
+ try {
8206
+ await this.injectLayoutNode(JSON.parse(JSON.stringify(this.designerClipboard)), parent);
8207
+ this.cd.detectChanges();
8208
+ WtoolboxService.messageNotificationService.add({
8209
+ severity: 'success',
8210
+ summary: this.t('success', 'Success'),
8211
+ detail: this.t('designer.clipboard.pasted', 'Elemento incollato')
8212
+ });
8213
+ }
8214
+ catch (error) {
8215
+ console.error('Failed to paste designer element', error);
8216
+ WtoolboxService.errorHandler?.handleError?.(error);
8217
+ }
8218
+ }
8219
+ /**
8220
+ * Duplica un elemento: lo copia e lo incolla come sibling (stesso parent).
8221
+ */
8222
+ async duplicateDesignerElement(el) {
8223
+ const target = el || this.tool || this.ctxElement;
8224
+ if (!target) {
8225
+ return;
8226
+ }
8227
+ this.copyDesignerElement(target);
8228
+ if (!this.designerClipboard) {
8229
+ // caso contenitore: copyDesignerElement ha gia' avvisato
8230
+ return;
8231
+ }
8232
+ const parentUn = this.findParentUniqueName(target);
8233
+ await this.pasteDesignerElement(parentUn);
8234
+ }
8235
+ /**
8236
+ * Esporta il boardcontent della dashboard corrente come file JSON scaricabile.
8237
+ * Riusa lo stesso choke-point di serializzazione del salvataggio
8238
+ * (`buildSerializedDashboardElements`), quindi il file prodotto e' identico al
8239
+ * `boardcontent` persistito su `dom_board` ed e' re-importabile qui o su un
8240
+ * altro ambiente (import/export tra ambienti).
8241
+ */
8242
+ exportDashboardJson() {
8243
+ try {
8244
+ const raw = this.buildSerializedDashboardElements();
8245
+ let pretty = raw;
8246
+ try {
8247
+ pretty = JSON.stringify(JSON.parse(raw), null, 2);
8248
+ }
8249
+ catch { /* keep raw */ }
8250
+ const routeSlug = String(this.currentDashboardRoute || 'dashboard')
8251
+ .replace(/[^a-zA-Z0-9._-]+/g, '_')
8252
+ .replace(/^_+|_+$/g, '') || 'dashboard';
8253
+ const fileName = `${routeSlug}.dashboard.json`;
8254
+ const blob = new Blob([pretty], { type: 'application/json' });
8255
+ const url = URL.createObjectURL(blob);
8256
+ const anchor = document.createElement('a');
8257
+ anchor.href = url;
8258
+ anchor.download = fileName;
8259
+ document.body.appendChild(anchor);
8260
+ anchor.click();
8261
+ document.body.removeChild(anchor);
8262
+ setTimeout(() => URL.revokeObjectURL(url), 0);
8263
+ WtoolboxService.messageNotificationService.add({
8264
+ severity: 'success',
8265
+ summary: this.t('success', 'Success'),
8266
+ detail: this.t('designer.export_json_success', 'Dashboard JSON exported')
8267
+ });
8268
+ }
8269
+ catch (error) {
8270
+ console.error('Failed to export dashboard JSON', error);
8271
+ WtoolboxService.errorHandler?.handleError?.(error);
8272
+ }
8273
+ }
8274
+ /**
8275
+ * Importa un boardcontent da un file JSON precedentemente esportato.
8276
+ * Apre un file picker nativo (input[type=file] creato al volo, nessun prompt
8277
+ * nativo del browser), valida il JSON, chiede conferma di sovrascrittura del
8278
+ * canvas corrente e infine ricostruisce la dashboard riusando `loadDashboard`
8279
+ * (con `skipServerFetch=true`), lo stesso path usato per aprire una dashboard
8280
+ * salvata — cosi' l'import passa da idratazione/backfill/rebind canonici.
8281
+ */
8282
+ importDashboardJson() {
8283
+ const input = document.createElement('input');
8284
+ input.type = 'file';
8285
+ input.accept = 'application/json,.json';
8286
+ input.style.display = 'none';
8287
+ input.addEventListener('change', () => {
8288
+ const file = input.files && input.files[0];
8289
+ document.body.removeChild(input);
8290
+ if (!file) {
8291
+ return;
8292
+ }
8293
+ const reader = new FileReader();
8294
+ reader.onload = () => {
8295
+ const text = String(reader.result || '');
8296
+ void this.applyImportedDashboardJson(text);
8297
+ };
8298
+ reader.onerror = () => {
8299
+ WtoolboxService.messageNotificationService.add({
8300
+ severity: 'error',
8301
+ summary: this.t('error', 'Error'),
8302
+ detail: this.t('designer.import_json_read_error', 'Could not read the selected file')
8303
+ });
8304
+ };
8305
+ reader.readAsText(file);
8306
+ });
8307
+ document.body.appendChild(input);
8308
+ input.click();
8309
+ }
8310
+ /**
8311
+ * Valida e applica un boardcontent importato. Estratta da `importDashboardJson`
8312
+ * per essere riusabile (es. incolla da clipboard / test) e testabile.
8313
+ */
8314
+ async applyImportedDashboardJson(text) {
8315
+ let parsed;
8316
+ try {
8317
+ parsed = JSON.parse(text);
8318
+ }
8319
+ catch {
8320
+ WtoolboxService.messageNotificationService.add({
8321
+ severity: 'error',
8322
+ summary: this.t('error', 'Error'),
8323
+ detail: this.t('designer.import_json_invalid', 'The file does not contain valid JSON')
8324
+ });
8325
+ return;
8326
+ }
8327
+ // Accetta sia l'array di elementi top-level sia il wrapper {elements|dashboardElements}
8328
+ const elements = Array.isArray(parsed)
8329
+ ? parsed
8330
+ : (Array.isArray(parsed?.elements)
8331
+ ? parsed.elements
8332
+ : (Array.isArray(parsed?.dashboardElements) ? parsed.dashboardElements : null));
8333
+ if (!elements) {
8334
+ WtoolboxService.messageNotificationService.add({
8335
+ severity: 'error',
8336
+ summary: this.t('error', 'Error'),
8337
+ detail: this.t('designer.import_json_unrecognized', 'The JSON is not a recognizable dashboard layout')
8338
+ });
8339
+ return;
8340
+ }
8341
+ if ((this.flattenedDashboardElements || []).length > 0) {
8342
+ const ok = await WtoolboxService.confirm({
8343
+ header: this.t('warning', 'Warning'),
8344
+ message: this.t('designer.import_json_confirm', 'Importing will replace the current canvas. Continue?'),
8345
+ acceptLabel: this.t('confirm', 'Confirm'),
8346
+ rejectLabel: this.t('cancel', 'Cancel')
8347
+ });
8348
+ if (!ok) {
8349
+ return;
8350
+ }
8351
+ }
8352
+ try {
8353
+ await this.loadDashboard({
8354
+ boardcontent: JSON.stringify(elements),
8355
+ board_route: this.currentDashboardRoute || '',
8356
+ board_des: this.currentDashboardDescription || ''
8357
+ }, true);
8358
+ WtoolboxService.messageNotificationService.add({
8359
+ severity: 'success',
8360
+ summary: this.t('success', 'Success'),
8361
+ detail: this.t('designer.import_json_success', 'Dashboard JSON imported')
8362
+ });
8363
+ }
8364
+ catch (error) {
8365
+ console.error('Failed to import dashboard JSON', error);
8366
+ WtoolboxService.errorHandler?.handleError?.(error);
8367
+ }
8368
+ }
8369
+ get droppedDashboardItems() {
8370
+ return (this.flattenedDashboardElements || []).filter((item) => !!item?.uniqueName);
8371
+ }
8372
+ get hierarchyTreeNodes() {
8373
+ return this.hierarchyTreeNodesState;
8374
+ }
8375
+ get canUndo() {
8376
+ return this.undoHistory.length > 0;
8377
+ }
8378
+ get canRedo() {
8379
+ return this.redoHistory.length > 0;
8380
+ }
8381
+ /**
8382
+ * Calcola la label visualizzata per un elemento nel pannello "dropped items".
8383
+ */
8384
+ getDroppedDashboardItemLabel(item) {
8385
+ const displayName = String(item?.displayName || '').trim();
8386
+ if (displayName) {
8387
+ return displayName;
8388
+ }
8389
+ const uniqueName = String(item?.uniqueName || '').trim();
8390
+ const name = String(item?.name || '').trim();
8391
+ if (!uniqueName) {
8392
+ return name;
8393
+ }
8394
+ return name ? `${uniqueName} (${name})` : uniqueName;
8395
+ }
8396
+ /**
8397
+ * Imposta come selezionato l'elemento cliccato nella lista elementi.
8398
+ */
8399
+ selectDashboardItem(item) {
8400
+ if (!item) {
8401
+ return;
8402
+ }
8403
+ this.tool = item;
8404
+ this.syncHierarchySelectionToCurrentTool();
8405
+ }
8406
+ // ===================== Multi-selezione (rubber-band) =====================
8407
+ /** Canvas host (per le coordinate del rettangolo di selezione). */
8408
+ canvasRef;
8409
+ /** Elementi top-level multi-selezionati. `tool` (attivo nel property panel) e' null quando != 1. */
8410
+ selectedTools = [];
8411
+ /** Overlay del rettangolo di selezione (rubber-band). */
8412
+ rubberBandVisible = false;
8413
+ rubberBandStyle = { left: '0px', top: '0px', width: '0px', height: '0px' };
8414
+ rubber = null;
8415
+ canvasRectAtStart = null;
8416
+ boundRubberMove = (e) => this.onRubberMove(e);
8417
+ boundRubberUp = (e) => this.onRubberUp(e);
8418
+ /**
8419
+ * Mousedown sul canvas: avvia la selezione a rettangolo SOLO se parte da area
8420
+ * vuota (nessun elemento sotto il puntatore). Sugli elementi il mousedown e'
8421
+ * consumato dalla direttiva di drag (`stopImmediatePropagation`), quindi qui
8422
+ * arriva solo il click su spazio libero — coerente col vincolo richiesto.
8423
+ */
8424
+ onCanvasMouseDown(event) {
8425
+ if (this.noEdit || !event || event.button !== 0) {
8426
+ return;
8427
+ }
8428
+ const canvasEl = this.canvasRef?.nativeElement;
8429
+ if (!canvasEl || !this.isEmptyCanvasTarget(event.target, canvasEl)) {
8430
+ return;
8431
+ }
8432
+ event.preventDefault();
8433
+ this.clearMultiSelection();
8434
+ this.tool = null;
8435
+ this.canvasRectAtStart = canvasEl.getBoundingClientRect();
8436
+ this.rubber = { startClientX: event.clientX, startClientY: event.clientY };
8437
+ this.rubberBandVisible = true;
8438
+ this.updateRubberBandStyle(event.clientX, event.clientY);
8439
+ window.addEventListener('mousemove', this.boundRubberMove);
8440
+ window.addEventListener('mouseup', this.boundRubberUp);
8441
+ this.cd.detectChanges();
8442
+ }
8443
+ /** true se il target risale fino al canvas senza attraversare un elemento designer (id con '__'). */
8444
+ isEmptyCanvasTarget(target, canvasEl) {
8445
+ let node = target;
8446
+ while (node && node !== canvasEl) {
8447
+ const id = node.getAttribute ? (node.getAttribute('id') || '') : '';
8448
+ if (id.indexOf('__') > -1) {
8449
+ return false;
8450
+ }
8451
+ node = node.parentElement;
8452
+ }
8453
+ return node === canvasEl;
8454
+ }
8455
+ onRubberMove(event) {
8456
+ if (!this.rubber) {
8457
+ return;
8458
+ }
8459
+ this.updateRubberBandStyle(event.clientX, event.clientY);
8460
+ this.cd.detectChanges();
8461
+ }
8462
+ updateRubberBandStyle(curX, curY) {
8463
+ if (!this.rubber || !this.canvasRectAtStart) {
8464
+ return;
8465
+ }
8466
+ const canvasEl = this.canvasRef?.nativeElement;
8467
+ const r = this.canvasRectAtStart;
8468
+ const sL = canvasEl ? canvasEl.scrollLeft : 0;
8469
+ const sT = canvasEl ? canvasEl.scrollTop : 0;
8470
+ const x1 = Math.min(this.rubber.startClientX, curX), x2 = Math.max(this.rubber.startClientX, curX);
8471
+ const y1 = Math.min(this.rubber.startClientY, curY), y2 = Math.max(this.rubber.startClientY, curY);
8472
+ this.rubberBandStyle = {
8473
+ left: (x1 - r.left + sL) + 'px',
8474
+ top: (y1 - r.top + sT) + 'px',
8475
+ width: (x2 - x1) + 'px',
8476
+ height: (y2 - y1) + 'px',
8477
+ };
8478
+ }
8479
+ onRubberUp(event) {
8480
+ window.removeEventListener('mousemove', this.boundRubberMove);
8481
+ window.removeEventListener('mouseup', this.boundRubberUp);
8482
+ const rubber = this.rubber;
8483
+ this.rubber = null;
8484
+ this.rubberBandVisible = false;
8485
+ if (!rubber) {
8486
+ this.cd.detectChanges();
8487
+ return;
8488
+ }
8489
+ const x1 = Math.min(rubber.startClientX, event.clientX), x2 = Math.max(rubber.startClientX, event.clientX);
8490
+ const y1 = Math.min(rubber.startClientY, event.clientY), y2 = Math.max(rubber.startClientY, event.clientY);
8491
+ const selected = [];
8492
+ // Ignora i "click" (rettangolo minuscolo): niente selezione, solo deselect.
8493
+ if (Math.abs(x2 - x1) >= 3 && Math.abs(y2 - y1) >= 3) {
8494
+ // Hit-test dei soli elementi TOP-LEVEL (spostare/eliminare un top-level porta i figli).
8495
+ for (const el of (this.dashboardElements || [])) {
8496
+ const dom = el?.uniqueName ? document.getElementById(el.uniqueName) : null;
8497
+ if (!dom) {
8498
+ continue;
8499
+ }
8500
+ const b = dom.getBoundingClientRect();
8501
+ if (b.left < x2 && b.right > x1 && b.top < y2 && b.bottom > y1) {
8502
+ selected.push(el);
8503
+ }
8504
+ }
8505
+ }
8506
+ this.setMultiSelection(selected);
8507
+ this.cd.detectChanges();
8508
+ }
8509
+ /** Imposta la multi-selezione: `tool` resta valorizzato solo con esattamente 1 elemento. */
8510
+ setMultiSelection(selected) {
8511
+ this.clearHighlight();
8512
+ this.selectedTools = selected || [];
8513
+ this.tool = this.selectedTools.length === 1 ? this.selectedTools[0] : null;
8514
+ this.applyHighlight();
8515
+ }
8516
+ clearMultiSelection() {
8517
+ this.clearHighlight();
8518
+ this.selectedTools = [];
8519
+ }
8520
+ applyHighlight() {
8521
+ for (const el of (this.selectedTools || [])) {
8522
+ const dom = el?.uniqueName ? document.getElementById(el.uniqueName) : null;
8523
+ if (dom) {
8524
+ dom.classList.add('designer-multi-selected');
8525
+ }
8526
+ }
8527
+ }
8528
+ clearHighlight() {
8529
+ for (const el of (this.selectedTools || [])) {
8530
+ const dom = el?.uniqueName ? document.getElementById(el.uniqueName) : null;
8531
+ if (dom) {
8532
+ dom.classList.remove('designer-multi-selected');
8533
+ }
8534
+ }
8535
+ }
8536
+ /** Applica un delta di traslazione (px) a un insieme di elementi (multi-move via frecce). */
8537
+ nudgeElements(tools, dx, dy) {
8538
+ for (const el of (tools || [])) {
8539
+ if (!el?.inputs) {
8540
+ continue;
8541
+ }
8542
+ let scaleX = 1, skewY = 0, skewX = 0, scaleY = 1, tx = 0, ty = 0;
8543
+ const m = String(el.inputs['transform'] || '').match(/matrix\(([^)]+)\)/);
8544
+ if (m) {
8545
+ const p = m[1].split(',').map((x) => parseFloat(x));
8546
+ if (p.length >= 6 && p.every((n) => !isNaN(n))) {
8547
+ [scaleX, skewY, skewX, scaleY, tx, ty] = p;
8548
+ }
8549
+ }
8550
+ el.inputs['transform'] = `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${tx + dx}, ${ty + dy})`;
8551
+ }
8552
+ this.commitHistoryIfChanged();
8553
+ this.cd.detectChanges();
8554
+ }
8555
+ // ============ Import dashboard/preset DENTRO un elemento (context menu) ============
8556
+ /** true se il tool e' un container che accetta figli arbitrari (nestedComponents + nessun allowedChildren). */
8557
+ isFreeContainer(tool) {
8558
+ return !!tool && Array.isArray(tool.nestedComponents) && !(tool.allowedChildren);
8559
+ }
8560
+ /** Voci di context menu per importare dashboard/preset dentro un container. */
8561
+ buildImportContextItems(el) {
8562
+ return [
8563
+ { separator: true },
8564
+ { label: this.t('designer.ctx_import_dashboard', 'Importa dashboard qui'), icon: 'pi pi-download', command: () => this.importDashboardIntoElement(el) },
8565
+ { label: this.t('designer.ctx_import_preset', 'Importa preset qui'), icon: 'pi pi-bookmark', command: () => this.importPresetIntoElement(el) },
8566
+ ];
8567
+ }
8568
+ /**
8569
+ * UNICA sorgente del context menu di un elemento, condivisa dai tre punti di
8570
+ * accesso (canvas `showCtx`, gerarchia `showHierarchyNodeContextMenu`, footer
8571
+ * breadcrumb `showFooterContextMenu`). Le `command` operano sull'elemento
8572
+ * passato (risolto al click), non su una variabile globale, quindi il menu si
8573
+ * ricostruisce per-elemento con voci identiche ovunque. Il caller canvas
8574
+ * antepone eventuali voci field-editor (specifiche del click su un campo).
8575
+ */
8576
+ buildElementContextMenuItems(el) {
8577
+ const items = [
8578
+ {
8579
+ label: this.t('designer.edit_properties', 'Edit properties'),
8580
+ icon: 'pi pi-cog',
8581
+ command: () => { if (el) {
8582
+ this.selectDashboardItem(el);
8583
+ this.cd.detectChanges();
8584
+ } }
8585
+ },
8586
+ {
8587
+ label: this.t('designer.duplicate', 'Duplica'),
8588
+ icon: 'pi pi-clone',
8589
+ command: () => { if (el) {
8590
+ void this.duplicateDesignerElement(el);
8591
+ } }
8592
+ },
8593
+ {
8594
+ label: this.t('designer.rename', 'Rename'),
8595
+ icon: 'pi pi-pencil',
8596
+ command: () => { if (el) {
8597
+ void this.renameDesignerElement(el);
8598
+ } }
8599
+ },
8600
+ {
8601
+ label: this.t('designer.remove', 'Remove'),
8602
+ icon: 'pi pi-trash',
8603
+ command: () => { const n = String(el?.uniqueName || '').trim(); if (n) {
8604
+ this.removeElementByName(n);
8605
+ } }
8606
+ }
8607
+ ];
8608
+ // Sposta in nuovo Tab: SOLO se l'elemento e' dentro un TABPANEL di un TABVIEW
8609
+ // (migrazione elemento -> nuovo TABPANEL del TABVIEW padre).
8610
+ if (this.isInsideTabPanel(el)) {
8611
+ items.push({
8612
+ label: this.t('designer.ctx_move_to_new_tab', 'Sposta in nuovo Tab'),
8613
+ icon: 'pi pi-folder-open',
8614
+ command: () => { void this.moveElementToNewTab(el); }
8615
+ });
8616
+ // Sposta in altro tab: solo se il TABVIEW ha piu' di un tab (scelta esclusa il corrente).
8617
+ if (this.hasOtherTabs(el)) {
8618
+ items.push({
8619
+ label: this.t('designer.ctx_move_to_existing_tab_element', 'Sposta in altro tab'),
8620
+ icon: 'pi pi-arrow-right-arrow-left',
8621
+ command: () => { void this.moveElementToExistingTab(el); }
8622
+ });
8623
+ }
8624
+ }
8625
+ // Import dashboard/preset: solo per container liberi (nestedComponents senza allowedChildren).
8626
+ if (this.isFreeContainer(el)) {
8627
+ items.push(...this.buildImportContextItems(el));
8628
+ }
8629
+ return items;
8630
+ }
8631
+ /**
8632
+ * Risolve il contesto tab di un elemento: il TABPANEL che lo contiene e il
8633
+ * TABVIEW che contiene quel pannello. null se l'elemento non e' dentro un tab.
8634
+ */
8635
+ resolveTabContext(el) {
8636
+ if (!el) {
8637
+ return null;
8638
+ }
8639
+ const panelUn = this.findParentUniqueName(el);
8640
+ const panel = panelUn ? this.getElementByName(panelUn) : null;
8641
+ if (!panel || String(panel.name || '').toUpperCase() !== 'TABPANEL') {
8642
+ return null;
8643
+ }
8644
+ const tabViewUn = this.findParentUniqueName(panel);
8645
+ const tabView = tabViewUn ? this.getElementByName(tabViewUn) : null;
8646
+ if (!tabView || String(tabView.name || '').toUpperCase() !== 'TABVIEW') {
8647
+ return null;
8648
+ }
8649
+ return { panel, tabView };
8650
+ }
8651
+ /** true se l'elemento e' dentro un TABPANEL di un TABVIEW (eleggibile a "Sposta in nuovo Tab"). */
8652
+ isInsideTabPanel(el) {
8653
+ return !!this.resolveTabContext(el);
8654
+ }
8655
+ /** true se il TABVIEW che contiene l'elemento ha piu' di un tab (eleggibile a "Sposta in altro tab"). */
8656
+ hasOtherTabs(el) {
8657
+ const ctx = this.resolveTabContext(el);
8658
+ return !!ctx && (ctx.tabView.nestedComponents?.length || 0) > 1;
8659
+ }
8660
+ /**
8661
+ * Sposta l'elemento in un NUOVO tab (TABPANEL) del TABVIEW che lo contiene:
8662
+ * crea un TABPANEL fresco (clonando il template di palette, uniqueName/componentId
8663
+ * nuovi), rimuove l'elemento dal tab corrente e ci mette lo STESSO oggetto (move,
8664
+ * non clone -> binding datasource/stato preservati), poi seleziona il nuovo tab.
8665
+ */
8666
+ async moveElementToNewTab(el) {
8667
+ if (!el) {
8668
+ return;
8669
+ }
8670
+ const ctx = this.resolveTabContext(el);
8671
+ if (!ctx) {
8672
+ return;
8673
+ }
8674
+ const { panel, tabView } = ctx;
8675
+ // Nome del nuovo tab (default: displayName/name dell'elemento).
8676
+ const resp = await WtoolboxService.promptDialog(this.t('designer.ctx_move_to_new_tab', 'Sposta in nuovo Tab'), [{
8677
+ name: 'tabName',
8678
+ caption: this.t('designer.tab_name_caption', 'Nome Tab'),
8679
+ type: 'text',
8680
+ required: true,
8681
+ value: String(el.displayName || el.name || 'Tab').trim()
8682
+ }], '440px', '320px');
8683
+ if (!resp) {
8684
+ return;
8685
+ }
8686
+ const tabName = String(resp?.tabName?.value || '').trim();
8687
+ if (!tabName) {
8688
+ return;
8689
+ }
8690
+ // Nuovo TABPANEL dalla stessa shape del drop (template + id freschi).
8691
+ const template = (this.availableTools || []).find((t) => String(t?.name || '').toUpperCase() === 'TABPANEL');
8692
+ if (!template) {
8693
+ return;
8694
+ }
8695
+ const newPanel = JSON.parse(JSON.stringify(template));
8696
+ newPanel.componentId = this.getNewID();
8697
+ newPanel.uniqueName = newPanel.name + '__' + newPanel.componentId;
8698
+ newPanel.inputs = newPanel.inputs || {};
8699
+ newPanel.inputs['header'] = tabName;
8700
+ newPanel.nestedComponents = [];
8701
+ // Move: rimuovi dall'attuale tab e inserisci lo stesso oggetto nel nuovo.
8702
+ panel.nestedComponents = (panel.nestedComponents || []).filter((k) => String(k?.uniqueName || '') !== String(el.uniqueName || ''));
8703
+ newPanel.nestedComponents.push(el);
8704
+ // Aggancia il nuovo tab al TABVIEW e selezionalo (deseleziona gli altri).
8705
+ tabView.nestedComponents = tabView.nestedComponents || [];
8706
+ tabView.nestedComponents.push(newPanel);
8707
+ this.selectTabPanelExclusive(newPanel);
8708
+ this.flattenedDashboardElements = this.flattenComponentTree(this.dashboardElements);
8709
+ this.commitHistoryIfChanged();
8710
+ this.cd.detectChanges();
8711
+ WtoolboxService.messageNotificationService.add({
8712
+ severity: 'success',
8713
+ summary: this.t('success', 'Success'),
8714
+ detail: this.t('designer.moved_to_new_tab', 'Elemento spostato nel nuovo tab')
8715
+ });
8716
+ }
8717
+ /**
8718
+ * Sposta l'elemento in un ALTRO tab (TABPANEL) gia' esistente dello stesso
8719
+ * TABVIEW: picker dei tab escluso il corrente, poi move dello STESSO oggetto
8720
+ * (binding/stato preservati) nel tab scelto, che viene selezionato.
8721
+ */
8722
+ async moveElementToExistingTab(el) {
8723
+ if (!el) {
8724
+ return;
8725
+ }
8726
+ const ctx = this.resolveTabContext(el);
8727
+ if (!ctx) {
8728
+ return;
8729
+ }
8730
+ const { panel, tabView } = ctx;
8731
+ // Tab candidati: tutti i TABPANEL del TABVIEW tranne quello corrente.
8732
+ const others = (tabView.nestedComponents || []).filter((p) => String(p?.uniqueName || '') !== String(panel.uniqueName || ''));
8733
+ if (!others.length) {
8734
+ return;
8735
+ }
8736
+ const choices = others.map((p, idx) => {
8737
+ const label = String(p?.inputs?.header || p?.displayName || p?.uniqueName || '').trim() || ('Tab ' + (idx + 1));
8738
+ return { label, value: String(p?.uniqueName || '') };
8739
+ });
8740
+ const resp = await WtoolboxService.promptDialog(this.t('designer.ctx_move_to_existing_tab_element', 'Sposta in altro tab'), [{
8741
+ name: 'targetTab',
8742
+ caption: this.t('designer.target_tab_caption', 'Tab di destinazione'),
8743
+ type: 'dictionary',
8744
+ required: true,
8745
+ value: choices[0].value,
8746
+ dictionaryData: choices
8747
+ }], '440px', '320px');
8748
+ if (!resp) {
8749
+ return;
8750
+ }
8751
+ const targetUn = String(resp?.targetTab?.value || '').trim();
8752
+ if (!targetUn) {
8753
+ return;
8754
+ }
8755
+ const targetPanel = (tabView.nestedComponents || []).find((p) => String(p?.uniqueName || '') === targetUn);
8756
+ if (!targetPanel) {
8757
+ return;
8758
+ }
8759
+ // Move: rimuovi dal tab corrente e inserisci lo stesso oggetto nel target.
8760
+ panel.nestedComponents = (panel.nestedComponents || []).filter((k) => String(k?.uniqueName || '') !== String(el.uniqueName || ''));
8761
+ targetPanel.nestedComponents = Array.isArray(targetPanel.nestedComponents) ? targetPanel.nestedComponents : [];
8762
+ targetPanel.nestedComponents.push(el);
8763
+ // Seleziona il tab di destinazione.
8764
+ this.selectTabPanelExclusive(targetPanel);
8765
+ this.flattenedDashboardElements = this.flattenComponentTree(this.dashboardElements);
8766
+ this.commitHistoryIfChanged();
8767
+ this.cd.detectChanges();
8768
+ WtoolboxService.messageNotificationService.add({
8769
+ severity: 'success',
8770
+ summary: this.t('success', 'Success'),
8771
+ detail: this.t('designer.moved_to_tab', 'Elemento spostato nel tab')
8772
+ });
8773
+ }
8774
+ /**
8775
+ * onDrop del TABPANEL: appena droppato dentro un TABVIEW lo rende il tab attivo,
8776
+ * cosi' la sua area diventa visibile e droppabile. Il pannello a questo punto e'
8777
+ * gia' stato inserito in `tabView.nestedComponents` dal drop() (push prima di onDrop).
8778
+ */
8779
+ dropTabPanel(currentTool) {
8780
+ this.selectTabPanelExclusive(currentTool);
8781
+ this.cd.detectChanges();
8782
+ }
8783
+ /**
8784
+ * Seleziona in modo esclusivo un TABPANEL nel TABVIEW che lo contiene
8785
+ * (`inputs.selected = true` sul pannello, `false` sugli altri). No-op se il
8786
+ * parent non e' un TABVIEW. Stesso meccanismo usato da `activateTabPanelForHover`.
8787
+ */
8788
+ selectTabPanelExclusive(panel) {
8789
+ const tabViewUn = this.findParentUniqueName(panel);
8790
+ const tabView = tabViewUn ? this.getElementByName(tabViewUn) : null;
8791
+ if (!tabView || String(tabView.name || '').toUpperCase() !== 'TABVIEW' || !Array.isArray(tabView.nestedComponents)) {
8792
+ return;
8793
+ }
8794
+ tabView.nestedComponents.forEach((p) => {
8795
+ if (!p?.inputs) {
8796
+ p.inputs = {};
8797
+ }
8798
+ p.inputs.selected = String(p?.uniqueName || '') === String(panel?.uniqueName || '');
8799
+ });
8800
+ }
8801
+ /** Importa una dashboard salvata DENTRO l'elemento (nei suoi nestedComponents). */
8802
+ async importDashboardIntoElement(target) {
8803
+ if (!this.isFreeContainer(target)) {
8804
+ return;
8805
+ }
8806
+ const options = (this.savedDashboardOptions || []).map((o) => ({ label: o.label, value: o.value }));
8807
+ if (!options.length) {
8808
+ WtoolboxService.messageNotificationService.add({ severity: 'info', summary: this.t('designer.import.title', 'Import'), detail: this.t('designer.import.no_dashboards', 'Nessuna dashboard salvata') });
8809
+ return;
8810
+ }
8811
+ const resp = await WtoolboxService.promptDialog(this.t('designer.import.pick_dashboard', 'Importa dashboard qui'), [{ name: 'dash', caption: this.t('designer.import.dashboard', 'Dashboard'), type: 'dictionary', dictionaryData: options, value: '', required: true }], null, '440px', '230px');
8812
+ const route = String(resp?.dash?.value ?? '').trim();
8813
+ if (!route) {
8814
+ return;
8815
+ }
8816
+ let boardcontent = null;
8817
+ try {
8818
+ const result = await this.dataSrv.loadDashboard({ user_id: this.userInfo.getuserInfo().user_id, dashRoute: route });
8819
+ const rows = Array.isArray(result) ? result : (Array.isArray(result?.results) ? result.results : (result ? [result] : []));
8820
+ boardcontent = rows?.[0]?.boardcontent ?? null;
8821
+ }
8822
+ catch {
8823
+ boardcontent = null;
8824
+ }
8825
+ await this.importBoardcontentIntoElement(target, boardcontent);
8826
+ }
8827
+ /** Importa un PRESET locale DENTRO l'elemento (nei suoi nestedComponents). */
8828
+ async importPresetIntoElement(target) {
8829
+ if (!this.isFreeContainer(target)) {
8830
+ return;
8831
+ }
8832
+ const presets = this.readDesignerPresets();
8833
+ const names = Object.keys(presets || {});
8834
+ if (!names.length) {
8835
+ WtoolboxService.messageNotificationService.add({ severity: 'info', summary: this.t('designer.import.title', 'Import'), detail: this.t('designer.import.no_presets', 'Nessun preset salvato') });
8836
+ return;
8837
+ }
8838
+ const resp = await WtoolboxService.promptDialog(this.t('designer.import.pick_preset', 'Importa preset qui'), [{ name: 'preset', caption: this.t('designer.import.preset', 'Preset'), type: 'dictionary', dictionaryData: names.map((n) => ({ label: n, value: n })), value: '', required: true }], null, '440px', '230px');
8839
+ const name = String(resp?.preset?.value ?? '').trim();
8840
+ if (!name || !presets[name]) {
8841
+ return;
8842
+ }
8843
+ await this.importBoardcontentIntoElement(target, presets[name]);
8844
+ }
8845
+ /**
8846
+ * Importa un boardcontent (dashboard salvata o preset) DENTRO un container:
8847
+ * rimappa uniqueName/componentId con id freschi (niente collisioni) e i riferimenti
8848
+ * che li puntano (datasource, parentDatasource, dropped-component...), poi riusa la
8849
+ * rehydration completa di loadDashboard (datasource inclusi) in modalita' insert-target.
8850
+ */
8851
+ async importBoardcontentIntoElement(target, boardContentRaw) {
8852
+ if (!target || !boardContentRaw) {
8853
+ WtoolboxService.messageNotificationService.add({ severity: 'warn', summary: this.t('designer.import.title', 'Import'), detail: this.t('designer.import.empty', 'Contenuto vuoto o non valido') });
8854
+ return;
8855
+ }
8856
+ let imported;
8857
+ try {
8858
+ imported = JSON.parse(boardContentRaw);
8859
+ }
8860
+ catch {
8861
+ imported = null;
8862
+ }
8863
+ if (!Array.isArray(imported)) {
8864
+ imported = Array.isArray(imported?.dashboardElements) ? imported.dashboardElements
8865
+ : Array.isArray(imported?.elements) ? imported.elements : [];
8866
+ }
8867
+ if (!imported.length) {
8868
+ WtoolboxService.messageNotificationService.add({ severity: 'warn', summary: this.t('designer.import.title', 'Import'), detail: this.t('designer.import.empty', 'Contenuto vuoto o non valido') });
8869
+ return;
8870
+ }
8871
+ // 1. Rimappa gli ID con id freschi (getNewID -> uniqueName = name__id) e traccia old->new.
8872
+ const nameMap = new Map();
8873
+ const idMap = new Map();
8874
+ this.remapImportedIds(imported, nameMap, idMap);
8875
+ // 2. Aggiorna i riferimenti agli elementi (datasource ecc.) coi nuovi uniqueName/id.
8876
+ this.remapImportedReferences(imported, nameMap, idMap);
8877
+ // 3. Riusa la rehydration completa di loadDashboard in modalita' insert-target.
8878
+ await this.loadDashboard({ boardcontent: JSON.stringify(imported), board_route: '' }, true, target);
8879
+ WtoolboxService.messageNotificationService.add({ severity: 'success', summary: this.t('designer.import.title', 'Import'), detail: this.t('designer.import.done', 'Contenuto importato nell\'elemento') });
8880
+ }
8881
+ /** Rimappa ricorsivamente componentId/uniqueName degli elementi importati con id freschi. */
8882
+ remapImportedIds(nodes, nameMap, idMap) {
8883
+ for (const el of (nodes || [])) {
8884
+ if (!el || typeof el !== 'object') {
8885
+ continue;
8886
+ }
8887
+ const oldName = String(el.uniqueName || '');
8888
+ const oldId = Number(el.componentId);
8889
+ const newId = this.getNewID();
8890
+ const newName = String(el.name || 'ITEM') + '__' + newId;
8891
+ if (oldName) {
8892
+ nameMap.set(oldName, newName);
8893
+ }
8894
+ if (!isNaN(oldId)) {
8895
+ idMap.set(oldId, newId);
8896
+ }
8897
+ el.componentId = newId;
8898
+ el.uniqueName = newName;
8899
+ if (Array.isArray(el.nestedComponents)) {
8900
+ this.remapImportedIds(el.nestedComponents, nameMap, idMap);
8901
+ }
8902
+ }
8903
+ }
8904
+ /** Aggiorna i riferimenti (oggetti con uniqueName/id) dentro gli inputs, coi nuovi valori. */
8905
+ remapImportedReferences(nodes, nameMap, idMap) {
8906
+ const SKIP = new Set(['nestedComponents', 'inputProps', 'toolProps', 'metaInfo', 'componentRef', 'propertyTree']);
8907
+ const remap = (v, depth) => {
8908
+ if (!v || typeof v !== 'object' || depth > 6) {
8909
+ return;
8910
+ }
8911
+ if (Array.isArray(v)) {
8912
+ for (const it of v) {
8913
+ remap(it, depth + 1);
8914
+ }
8915
+ return;
8916
+ }
8917
+ if (typeof v.uniqueName === 'string' && nameMap.has(v.uniqueName)) {
8918
+ v.uniqueName = nameMap.get(v.uniqueName);
8919
+ if (typeof v.id === 'number' && idMap.has(v.id)) {
8920
+ v.id = idMap.get(v.id);
8921
+ }
8922
+ if (typeof v.componentId === 'number' && idMap.has(v.componentId)) {
8923
+ v.componentId = idMap.get(v.componentId);
8924
+ }
8925
+ }
8926
+ for (const k of Object.keys(v)) {
8927
+ if (SKIP.has(k)) {
8928
+ continue;
8929
+ }
8930
+ remap(v[k], depth + 1);
8931
+ }
8932
+ };
8933
+ const walk = (list) => {
8934
+ for (const el of (list || [])) {
8935
+ if (!el) {
8936
+ continue;
8937
+ }
8938
+ remap(el.inputs, 0);
8939
+ if (Array.isArray(el.nestedComponents)) {
8940
+ walk(el.nestedComponents);
8941
+ }
8942
+ }
8943
+ };
8944
+ walk(nodes);
8945
+ }
8946
+ showHierarchyNodeContextMenu(event, node, menu) {
8947
+ event.preventDefault();
8948
+ event.stopPropagation();
8949
+ const uniqueName = String(node?.key || '').trim();
8950
+ this.hierarchyCtxElement = uniqueName ? this.getElementByName(uniqueName) : null;
8951
+ this.hierarchyCtxItems = this.buildElementContextMenuItems(this.hierarchyCtxElement);
8952
+ menu.show(event);
8953
+ }
8954
+ onHierarchyNodeSelect(event) {
8955
+ const node = event?.node;
8956
+ if (!node) {
8957
+ return;
8958
+ }
8959
+ const uniqueName = String(node?.key || '').trim();
8960
+ if (!uniqueName) {
8961
+ return;
8962
+ }
8963
+ const item = this.getElementByName(uniqueName);
8964
+ if (!item) {
8965
+ return;
8966
+ }
8967
+ this.selectDashboardItem(item);
8968
+ this.cd.detectChanges();
8969
+ }
8970
+ onHierarchyNodeMouseEnter(node) {
8971
+ const uniqueName = String(node?.key || '').trim();
8972
+ if (!uniqueName) {
8973
+ return;
8974
+ }
8975
+ this.setHierarchyHoverHighlightByKey(uniqueName);
8976
+ }
8977
+ onHierarchyNodeMouseLeave() {
8978
+ this.clearHierarchyHoverHighlight();
8979
+ }
8980
+ isHierarchyNodeSelected(node) {
8981
+ const nodeKey = String(node?.key || '').trim();
8982
+ const selectedKey = String(this.hierarchySelection?.key || '').trim();
8983
+ return !!nodeKey && nodeKey === selectedKey;
8984
+ }
8985
+ setHierarchyHoverHighlightByKey(uniqueName) {
8986
+ const key = String(uniqueName || '').trim();
8987
+ this.clearHierarchyHoverHighlight();
8988
+ if (!key) {
8989
+ return;
8990
+ }
8991
+ const item = this.getElementByName(key);
8992
+ if (!item) {
8993
+ return;
8994
+ }
8995
+ this.setFooterHoverHighlight(item);
8996
+ if (typeof document === 'undefined') {
8997
+ return;
8998
+ }
8999
+ const footerElement = document.querySelector(`.designer-footer-item[data-designer-unique-name="${key}"]`);
9000
+ if (footerElement) {
9001
+ footerElement.classList.add('designer-tree-hover-target');
9002
+ this.hierarchyHoverFooterElement = footerElement;
7745
9003
  }
7746
9004
  }
7747
9005
  clearHierarchyHoverHighlight() {
@@ -7946,13 +9204,14 @@ class DesignerComponent {
7946
9204
  event.preventDefault();
7947
9205
  event.stopPropagation();
7948
9206
  this.footerCtxElement = item;
9207
+ this.footerCtxItems = this.buildElementContextMenuItems(item);
7949
9208
  menu.show(event);
7950
9209
  }
7951
9210
  /**
7952
- * Rinomina l'elemento selezionato nel footer context menu aggiornando `displayName`.
9211
+ * Rinomina un elemento aggiornando `displayName`. Usato dal context menu
9212
+ * unificato (canvas/gerarchia/footer) via `buildElementContextMenuItems`.
7953
9213
  */
7954
- async renameFooterContextItem() {
7955
- const item = this.footerCtxElement;
9214
+ async renameDesignerElement(item) {
7956
9215
  if (!item) {
7957
9216
  return;
7958
9217
  }
@@ -8428,12 +9687,9 @@ class DesignerComponent {
8428
9687
  const fieldCtxItems = (contextField && contextDs)
8429
9688
  ? this.buildFieldEditorContextMenuItems(contextField, contextDs)
8430
9689
  : [];
8431
- if (el && el.ctxItems) {
8432
- this.ctxItems.next([...fieldCtxItems, ...el.ctxItems]);
8433
- }
8434
- else {
8435
- this.ctxItems.next([...fieldCtxItems, ...this.defaultCtxItems]);
8436
- }
9690
+ // Menu elemento dalla sorgente unificata (edit/duplica/rinomina/rimuovi +
9691
+ // import per i container); il canvas antepone le voci field-editor.
9692
+ this.ctxItems.next([...fieldCtxItems, ...this.buildElementContextMenuItems(el)]);
8437
9693
  if (el) {
8438
9694
  this.ctxElement = el;
8439
9695
  }
@@ -9230,6 +10486,36 @@ class DesignerComponent {
9230
10486
  this.closeArchetypeConfigurator();
9231
10487
  return;
9232
10488
  }
10489
+ this.applyArchetypeConfigToTool(tool, action, nextConfig);
10490
+ this.closeArchetypeConfigurator();
10491
+ }
10492
+ /**
10493
+ * Core riusabile di {@link onArchetypeConfigApply}: applica la config di
10494
+ * archetipo a un DATAREPEATER tool. Scrive `customProps_<action>` sulle
10495
+ * proprietà del tool (= ciò che il chatbot designer_inject persiste e l'archetipo
10496
+ * legge), aggiorna `metaInfo.tableMetadata.extraProps`+`md_props_bag`, e RIGENERA
10497
+ * il render chiamando `ds.fetchData()` (re-emit `fetchInfo$` → l'archetipo
10498
+ * ri-legge type/opzioni) + `propertyTreeBuilder`. NESSUNA chiamata server.
10499
+ * Usato sia dal configuratore UI ("Configura chart") sia dall'injection del
10500
+ * chatbot (drop di un DATAREPEATER con `archetype_config`).
10501
+ */
10502
+ applyArchetypeConfigToTool(tool, action, nextConfig) {
10503
+ if (!tool || !action || !nextConfig)
10504
+ return;
10505
+ // Chart AGGREGATO: con groupInfo/aggregates il backend ritorna il valore per-gruppo nella
10506
+ // colonna `<field>_<AGG>` (es. LatestRecordedPopulation_AVG), NON nel campo grezzo (null nelle
10507
+ // righe gruppo). Il modello/configuratore emette il campo grezzo come dataField + l'aggregato in
10508
+ // `aggregates`: qui colleghiamo i due riscrivendo il dataField dei datasets sul nome aggregato,
10509
+ // cosi' il chart plotta i valori per gruppo (suffix uppercase, coerente con _Metadati_methods).
10510
+ if (Array.isArray(nextConfig.aggregates) && nextConfig.aggregates.length && nextConfig.dataOptions?.datasets) {
10511
+ for (const dset of nextConfig.dataOptions.datasets) {
10512
+ const agg = nextConfig.aggregates.find((a) => a?.field === dset?.dataField);
10513
+ const sfx = agg?.aggregate ? '_' + String(agg.aggregate).toUpperCase() : '';
10514
+ if (sfx && dset?.dataField && !String(dset.dataField).toUpperCase().endsWith(sfx)) {
10515
+ dset.dataField = dset.dataField + sfx;
10516
+ }
10517
+ }
10518
+ }
9233
10519
  const customKey = 'customProps_' + action;
9234
10520
  tool.inputs[customKey] = this.toBehaviorSubjectsObject(nextConfig);
9235
10521
  const metaInfo = this.getDesignerDatasourceMetaInfo(tool);
@@ -9238,15 +10524,33 @@ class DesignerComponent {
9238
10524
  extraProps.archetypes = extraProps.archetypes || {};
9239
10525
  const currentArchetype = extraProps.archetypes[action] || {};
9240
10526
  extraProps.archetypes[action] = Object.assign({}, currentArchetype, nextConfig);
10527
+ // AGGREGAZIONE chart: groupInfo/aggregates sono config DATASOURCE-level (extraProps
10528
+ // top-level, lette in data-source.component initMetadata:1914), non archetype-level. Se
10529
+ // l'archetype_config li porta (es. chart 'popolazione media per provincia'), sollevali a
10530
+ // extraProps top-level cosi' il datasource raggruppa/aggrega lato server nel fetch.
10531
+ if (Array.isArray(nextConfig.groupInfo))
10532
+ extraProps.groupInfo = nextConfig.groupInfo;
10533
+ if (Array.isArray(nextConfig.aggregates))
10534
+ extraProps.aggregates = nextConfig.aggregates;
9241
10535
  metaInfo.tableMetadata.extraProps = extraProps;
9242
10536
  metaInfo.tableMetadata.md_props_bag = JSON.stringify(extraProps);
9243
10537
  }
9244
10538
  const ds = tool.inputs?.['datasource']?.component?.value;
10539
+ // Il datasource parsa extraProps.groupInfo/aggregates SOLO all'init metadata: qui (config
10540
+ // applicato a runtime dall'injection/configuratore) li settiamo a mano sull'istanza PRIMA
10541
+ // del fetch, cosi' la fetchData successiva raggruppa/aggrega lato server (un punto per gruppo).
10542
+ if (ds) {
10543
+ if (Array.isArray(nextConfig.groupInfo)) {
10544
+ ds.groupInfo = nextConfig.groupInfo.map((x) => ({ field: x?.field, groupFormula: x?.groupFormula, dir: x?.dir, groupCount: x?.groupCount }));
10545
+ }
10546
+ if (Array.isArray(nextConfig.aggregates)) {
10547
+ ds.aggregationInfo = nextConfig.aggregates.map((x) => ({ field: x?.field, aggregate: x?.aggregate }));
10548
+ }
10549
+ }
9245
10550
  if (ds?.fetchData) {
9246
10551
  ds.fetchData();
9247
10552
  }
9248
10553
  this.propertyTreeBuilder(action, tool.inputs);
9249
- this.closeArchetypeConfigurator();
9250
10554
  }
9251
10555
  /**
9252
10556
  * Espone i metadati datasource al popup configuratore archetype.
@@ -10436,6 +11740,7 @@ Display formula context:
10436
11740
  });
10437
11741
  }
10438
11742
  let destination = this.dashboardElements;
11743
+ let parentContainerTool = null;
10439
11744
  if (event.toElement) {
10440
11745
  let id = event.toElement.getAttribute("id");
10441
11746
  let parentEl = event.toElement.parentElement;
@@ -10451,6 +11756,7 @@ Display formula context:
10451
11756
  if (match) {
10452
11757
  if (!match.allowedChildren || match.allowedChildren.includes(currentTool.name)) {
10453
11758
  destination = match?.nestedComponents;
11759
+ parentContainerTool = match;
10454
11760
  }
10455
11761
  else {
10456
11762
  WtoolboxService.messageNotificationService.add({
@@ -10476,11 +11782,68 @@ Display formula context:
10476
11782
  currentTool.onDrop(currentTool);
10477
11783
  }
10478
11784
  this.tool = currentTool;
11785
+ // Flusso assoluto (feature, default OFF): l'elemento droppato nasce
11786
+ // position:absolute con left/top dalla posizione del drop -> fuori dal flusso,
11787
+ // quindi ridimensionarne uno non sposta gli altri.
11788
+ if (this.absoluteFlowEnabled) {
11789
+ // Riferimento di posizionamento: il container se droppo dentro uno, altrimenti il canvas.
11790
+ let refEl = this.canvasRef?.nativeElement || null;
11791
+ if (parentContainerTool) {
11792
+ const containerDom = document.getElementById(parentContainerTool.uniqueName);
11793
+ if (containerDom) {
11794
+ refEl = containerDom;
11795
+ // Il container diventa il contesto di posizionamento del figlio absolute
11796
+ // (se non e' gia' posizionato): cosi' left/top del figlio sono relativi a lui.
11797
+ const curPos = String(parentContainerTool.inputs?.['position'] || '').trim();
11798
+ if (!curPos || curPos === 'static') {
11799
+ parentContainerTool.inputs['position'] = 'relative';
11800
+ }
11801
+ }
11802
+ }
11803
+ const pos = this.getDropPositionRelativeTo(event, refEl);
11804
+ currentTool.inputs['position'] = 'absolute';
11805
+ currentTool.inputs['left'] = pos.left + 'px';
11806
+ currentTool.inputs['top'] = pos.top + 'px';
11807
+ }
10479
11808
  this.flattenedDashboardElements = this.flattenComponentTree(this.dashboardElements);
11809
+ // Snap-on-drop SOLO in flusso normale: in modalita' assoluta la posizione e'
11810
+ // gia' settata (left/top) e snappata sopra.
11811
+ if (!this.absoluteFlowEnabled) {
11812
+ this.snapDroppedElementToGrid(currentTool);
11813
+ }
10480
11814
  this.commitHistoryIfChanged();
10481
11815
  this.draggedPayload = null;
10482
11816
  }
10483
11817
  }
11818
+ /**
11819
+ * Snap-on-drop: se lo snap alla griglia e' attivo, allinea la posizione
11820
+ * dell'elemento appena droppato al multiplo di griglia scrivendo un
11821
+ * `inputs.transform` di traslazione (stessa proprieta' usata da `onMoving`,
11822
+ * quindi persiste su save/reload). Deferred perche' l'elemento deve essere
11823
+ * nel DOM per leggere `offsetLeft/offsetTop` (posizione di flusso, senza
11824
+ * transform). No-op se snap off o gia' allineato.
11825
+ */
11826
+ snapDroppedElementToGrid(tool) {
11827
+ const g = DesignerSnapState.gridSize;
11828
+ if (!g || g <= 0 || !tool?.uniqueName) {
11829
+ return;
11830
+ }
11831
+ setTimeout(() => {
11832
+ const el = document.getElementById(tool.uniqueName);
11833
+ if (!el) {
11834
+ return;
11835
+ }
11836
+ const dx = Math.round(el.offsetLeft / g) * g - el.offsetLeft;
11837
+ const dy = Math.round(el.offsetTop / g) * g - el.offsetTop;
11838
+ if (dx === 0 && dy === 0) {
11839
+ return;
11840
+ }
11841
+ tool.inputs = tool.inputs || {};
11842
+ tool.inputs['transform'] = `matrix(1, 0, 0, 1, ${dx}, ${dy})`;
11843
+ this.cd.detectChanges();
11844
+ this.commitHistoryIfChanged();
11845
+ }, 0);
11846
+ }
10484
11847
  /**
10485
11848
  * Genera un nuovo identificativo incrementale per `componentId`.
10486
11849
  */
@@ -10769,7 +12132,80 @@ Display formula context:
10769
12132
  * - `elements` (JSON) -> `boardcontent` (TEXT)
10770
12133
  * e aggiorna anche i fogli collegati in `dom_board_sheet`.
10771
12134
  */
12135
+ /**
12136
+ * Lint pre-save del boardcontent: individua problemi di configurazione che
12137
+ * altrimenti emergerebbero solo a runtime. Metodo puro (nessun side-effect),
12138
+ * riusabile e testabile. Rileva:
12139
+ * - uniqueName duplicati (rompono selezione/rimozione/hierarchy tree, key=uniqueName);
12140
+ * - binding datasource orfani (DATAREPEATER/SELECT/MULTISELECT/UL che puntano a
12141
+ * un DATASOURCE.uniqueName inesistente nel layout);
12142
+ * - DATASOURCE senza route impostata.
12143
+ */
12144
+ computeDashboardLintIssues() {
12145
+ const issues = [];
12146
+ const flat = this.flattenComponentTree(this.dashboardElements) || [];
12147
+ // 1. uniqueName duplicati
12148
+ const seen = new Set();
12149
+ const dup = new Set();
12150
+ for (const el of flat) {
12151
+ const un = String(el?.uniqueName || '').trim();
12152
+ if (!un) {
12153
+ continue;
12154
+ }
12155
+ if (seen.has(un)) {
12156
+ dup.add(un);
12157
+ }
12158
+ seen.add(un);
12159
+ }
12160
+ for (const un of dup) {
12161
+ issues.push(this.trslSrv.format(this.t('designer.lint.duplicate_unique_name', "Nome elemento duplicato: '{0}'"), un));
12162
+ }
12163
+ // 2. uniqueName dei DATASOURCE disponibili
12164
+ const dsNames = new Set();
12165
+ for (const el of flat) {
12166
+ if (el?.name === 'DATASOURCE') {
12167
+ const un = String(el?.uniqueName || '').trim();
12168
+ if (un) {
12169
+ dsNames.add(un);
12170
+ }
12171
+ }
12172
+ }
12173
+ // 3. binding orfani + DATASOURCE senza route
12174
+ for (const el of flat) {
12175
+ const name = String(el?.name || '');
12176
+ if (name === 'DATAREPEATER' || name === 'SELECT' || name === 'MULTISELECT' || name === 'UL') {
12177
+ const ds = el?.inputs?.['datasource'];
12178
+ const dsUn = ds && typeof ds === 'object' ? String(ds.uniqueName || '').trim() : '';
12179
+ if (dsUn && !dsNames.has(dsUn)) {
12180
+ issues.push(this.trslSrv.format(this.t('designer.lint.orphan_datasource_binding', "Il componente '{0}' punta a un datasource inesistente ('{1}')"), String(el?.uniqueName || el?.name || ''), dsUn));
12181
+ }
12182
+ }
12183
+ if (name === 'DATASOURCE') {
12184
+ const routeVal = el?.inputs?.['route'];
12185
+ const routeStr = routeVal instanceof BehaviorSubject ? routeVal.value : routeVal;
12186
+ if (!String(routeStr || '').trim()) {
12187
+ issues.push(this.trslSrv.format(this.t('designer.lint.datasource_without_route', "Il datasource '{0}' non ha una route impostata"), String(el?.uniqueName || 'DATASOURCE')));
12188
+ }
12189
+ }
12190
+ }
12191
+ return issues;
12192
+ }
10772
12193
  async saveDashboard() {
12194
+ // Lint pre-save non bloccante: se ci sono problemi, elencali e chiedi conferma.
12195
+ const lintIssues = this.computeDashboardLintIssues();
12196
+ if (lintIssues.length > 0) {
12197
+ const proceed = await WtoolboxService.confirm({
12198
+ header: this.t('designer.lint.title', 'Problemi nel layout'),
12199
+ message: this.t('designer.lint.intro', 'Il layout presenta alcuni problemi:')
12200
+ + ' ' + lintIssues.map((i) => '• ' + i).join(' ')
12201
+ + ' — ' + this.t('designer.lint.confirm_save_anyway', 'Salvare comunque?'),
12202
+ acceptLabel: this.t('confirm', 'Confirm'),
12203
+ rejectLabel: this.t('cancel', 'Cancel')
12204
+ });
12205
+ if (!proceed) {
12206
+ return;
12207
+ }
12208
+ }
10773
12209
  const route = String(this.currentDashboardRoute || '').trim();
10774
12210
  if (route) {
10775
12211
  const payload = {
@@ -10855,14 +12291,18 @@ Display formula context:
10855
12291
  * e ricostruisce lo stato runtime del designer partendo da `dom_board.boardcontent`.
10856
12292
  * Supporta route in forme alias (`board_route`, `boardroute`, `dashRoute`).
10857
12293
  */
10858
- async loadDashboard(dashboard, skipServerFetch = false) {
12294
+ async loadDashboard(dashboard, skipServerFetch = false, insertTarget = null) {
10859
12295
  const dashRoute = String(dashboard?.board_route ??
10860
12296
  dashboard?.boardroute ??
10861
12297
  dashboard?.boardRoute ??
10862
12298
  dashboard?.dashRoute ??
10863
12299
  dashboard ??
10864
12300
  '').trim();
10865
- if (!dashRoute) {
12301
+ // Import/preset (feature C/F) passano boardcontent con route eventualmente
12302
+ // vuota (board non ancora salvata). NON fare reset in quel caso: il reset va
12303
+ // fatto solo quando NON c'e' ne' route ne' contenuto (nuovo grafo / clear).
12304
+ const providedBoardcontent = dashboard?.boardcontent ?? dashboard?.boardContent ?? null;
12305
+ if (!dashRoute && !providedBoardcontent) {
10866
12306
  this.dashboardElements = [];
10867
12307
  this.flattenedDashboardElements = [];
10868
12308
  this.currentDashboardRoute = '';
@@ -10876,8 +12316,10 @@ Display formula context:
10876
12316
  this.cd.detectChanges();
10877
12317
  return;
10878
12318
  }
10879
- this.currentDashboardRoute = dashRoute;
10880
- this.refreshGraphActionsMenuItems();
12319
+ if (!insertTarget) {
12320
+ this.currentDashboardRoute = dashRoute;
12321
+ this.refreshGraphActionsMenuItems();
12322
+ }
10881
12323
  let resultRows = [];
10882
12324
  let result = null;
10883
12325
  if (!skipServerFetch) {
@@ -10890,14 +12332,16 @@ Display formula context:
10890
12332
  ? result
10891
12333
  : (Array.isArray(result?.results) ? result.results : (result ? [result] : []));
10892
12334
  }
10893
- this.currentDashboardDescription = String(dashboard?.board_des ??
10894
- dashboard?.boarddes ??
10895
- resultRows?.[0]?.board_des ??
10896
- resultRows?.[0]?.boarddes ??
10897
- this.currentDashboardDescription ??
10898
- dashRoute).trim();
10899
- const boardSheetPaths = this.extractDashboardSheetPaths(resultRows, dashboard);
10900
- await this.loadDashboardCssSheets(boardSheetPaths);
12335
+ if (!insertTarget) {
12336
+ this.currentDashboardDescription = String(dashboard?.board_des ??
12337
+ dashboard?.boarddes ??
12338
+ resultRows?.[0]?.board_des ??
12339
+ resultRows?.[0]?.boarddes ??
12340
+ this.currentDashboardDescription ??
12341
+ dashRoute).trim();
12342
+ const boardSheetPaths = this.extractDashboardSheetPaths(resultRows, dashboard);
12343
+ await this.loadDashboardCssSheets(boardSheetPaths);
12344
+ }
10901
12345
  const boardContentRaw = resultRows?.[0]?.boardcontent ?? dashboard?.boardcontent ?? dashboard?.boardContent ?? null;
10902
12346
  if (boardContentRaw) {
10903
12347
  let loadedDashboard = [];
@@ -10915,7 +12359,9 @@ Display formula context:
10915
12359
  : [];
10916
12360
  }
10917
12361
  this.mergeMissingToolPropsRecursive(loadedDashboard);
10918
- this.dashboardComponents = [];
12362
+ if (!insertTarget) {
12363
+ this.dashboardComponents = [];
12364
+ }
10919
12365
  let dsSubs = [];
10920
12366
  let dsElements = [];
10921
12367
  let dsElementCount = (loadedDashboard || []).filter(x => x?.name == 'DATASOURCE').length;
@@ -11057,18 +12503,34 @@ Display formula context:
11057
12503
  });
11058
12504
  // Ensure a new reference for template control-flow and force refresh when
11059
12505
  // async loading happens outside Angular zone.
11060
- this.dashboardElements = Array.isArray(loadedDashboard) ? [...loadedDashboard] : [];
11061
- this.flattenedDashboardElements = flattened;
12506
+ if (insertTarget) {
12507
+ // Import IN-elemento: inserisci gli elementi importati nei nestedComponents
12508
+ // del target invece di sostituire l'intero canvas.
12509
+ insertTarget.nestedComponents = Array.isArray(insertTarget.nestedComponents) ? insertTarget.nestedComponents : [];
12510
+ insertTarget.nestedComponents.push(...(Array.isArray(loadedDashboard) ? loadedDashboard : []));
12511
+ this.dashboardElements = Array.isArray(this.dashboardElements) ? [...this.dashboardElements] : [];
12512
+ this.flattenedDashboardElements = this.flattenComponentTree(this.dashboardElements);
12513
+ }
12514
+ else {
12515
+ this.dashboardElements = Array.isArray(loadedDashboard) ? [...loadedDashboard] : [];
12516
+ this.flattenedDashboardElements = flattened;
12517
+ }
11062
12518
  this.cd.detectChanges();
11063
12519
  }
11064
- else {
12520
+ else if (!insertTarget) {
11065
12521
  this.dashboardElements = [];
11066
12522
  this.flattenedDashboardElements = [];
11067
12523
  this.currentDashboardRoute = '';
11068
12524
  this.cd.detectChanges();
11069
12525
  console.warn('[Designer] No dashboard content found for route:', dashRoute, result);
11070
12526
  }
11071
- this.resetHistory();
12527
+ if (insertTarget) {
12528
+ // Import in-elemento: NON resettare la history (stiamo aggiungendo), committa.
12529
+ this.commitHistoryIfChanged();
12530
+ }
12531
+ else {
12532
+ this.resetHistory();
12533
+ }
11072
12534
  this.refreshGraphActionsMenuItems();
11073
12535
  this.maxId = (this.flattenedDashboardElements || []).reduce((max, item) => Math.max(max, Number(item?.componentId || 0)), 0);
11074
12536
  }
@@ -11828,11 +13290,11 @@ Display formula context:
11828
13290
  // return customProps;
11829
13291
  }
11830
13292
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DesignerComponent, deps: [{ token: i1$1.ActivatedRoute }, { token: DataProviderService }, { token: MetadataProviderService }, { token: UserInfoService }, { token: i0.ChangeDetectorRef }, { token: TranslationManagerService }, { token: MetadataEditorService }, { token: ChatbotHostRegistryService }, { token: WorkflowRuntimeMetadataService }], target: i0.ɵɵFactoryTarget.Component });
11831
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DesignerComponent, isStandalone: true, selector: "wuic-designer", host: { listeners: { "window:hashchange": "onWindowHashChange()", "window:beforeunload": "onWindowBeforeUnload($event)", "window:keydown": "onWindowKeydown($event)", "window:mouseup": "onWindowMouseup()", "window:touchend": "onWindowTouchend()", "window:keyup": "onWindowKeyup()", "window:change": "onWindowChange()" } }, providers: [TreeDragDropService], ngImport: i0, template: "<!-- <button pButton type=\"button\" (click)=\"loadDashboard()\" class=\"mr-2\">{{'load' | translate}}</button> -->\n@if (!noEdit) {\n<div class=\"designer-layout\">\n <div class=\"designer-toolbar\">\n <div class=\"designer-current-dashboard\">{{ currentDashboardTitle }}</div>\n\n <div class=\"designer-toolbar-spacer\"></div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"undo()\" [disabled]=\"!canUndo\">{{ 'designer.undo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"redo()\" [disabled]=\"!canRedo\">{{ 'designer.redo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"refreshGraphActionsMenuItems(); graphActionsMenu.toggle($event)\">{{ 'designer.actions_menu' | translate }}</button>\n <p-menu #graphActionsMenu [popup]=\"true\" [model]=\"graphActionsMenuItems\" appendTo=\"body\"></p-menu>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"openCurrentDashboardInNewTab()\" [disabled]=\"!currentDashboardRoute\">{{\n 'designer.open_current_dashboard' | translate }}</button>\n </div>\n </div>\n <div class=\"designer-main\">\n <p-splitter [style]=\"{ height: '100%' }\" [panelSizes]=\"[15, 85]\" styleClass=\"designer-main-splitter\">\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[35, 65]\"\n [minSizes]=\"[20, 30]\" styleClass=\"designer-left-splitter\">\n <ng-template pTemplate>\n <div class=\"col flex designer-toolbox palette-panel\">\n <ul class=\"tool-groups\">\n @for (toolGroup of availableToolGroups; track toolGroup.group) {\n <li class=\"tool-group-item\">\n <div class=\"tool-group-title\">{{ toolGroup.group }}</div>\n <ul class=\"tool-group-list\">\n @for (tool of toolGroup.tools; track tool) {\n <li pDraggable (onDragStart)=\"dragStart(tool)\" (onDragEnd)=\"dragEnd()\" (onDrag)=\"onDrag($event)\"\n (dblclick)=\"dropToolAtRoot(tool)\">\n <i [ngClass]=\"tool.icon\" [title]=\"tool.name\"></i>\n </li>\n }\n </ul>\n </li>\n }\n </ul>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[68, 32]\"\n [minSizes]=\"[35, 20]\" styleClass=\"designer-left-bottom-splitter\">\n <ng-template pTemplate>\n <div class=\"properties-panel\">\n <p-select [style]=\"{width: '100%'}\" [options]=\"flattenedDashboardElements\" [(ngModel)]=\"tool\"\n optionLabel=\"uniqueName\" dataKey=\"componentId\" appendTo=\"body\" />\n <div class=\"designer-toolbox\">\n\n <span style=\"width: 100%;\">\n <input type=\"text\" style=\"width: calc(100% - 30px);\" [(ngModel)]=\"propFilter\"\n [placeholder]=\"'prop.filter' | translate\" />\n <button style=\"width: 25px\" (click)=\"clearPropFilter()\">X</button>\n </span>\n\n @for (section of getToolPropertySections(tool, propFilter); track section.key) {\n <details class=\"prop-section\" [open]=\"section.open\"\n (toggle)=\"onPropertySectionToggle(tool, section.key, $event)\">\n <summary class=\"prop-section-title\">{{ section.label }}</summary>\n <ul class=\"props\">\n @for (toolProp of section.props; track toolProp.key) {\n <li>\n @if (!toolProp.value.hideCaption) {\n <span>{{ toolProp.value?.propertyCaption ||\n toolProp.key[0].toUpperCase() + toolProp.key.slice(1)\n }}</span>\n }\n <br />\n @if (toolProp.value.type == 'button') {\n <p-button (click)=\"toolProp.value.callback(tool.inputs, toolProp.key, null, tool)\"\n class=\"k-button\" [style]=\"{width: '100%'}\">\n {{toolProp.value.propertyCaption}}\n </p-button>\n }\n @if (toolProp.value.type == 'metaEditor' &&\n tool.inputs['componentRef']?.value?.component) {\n <wuic-metadata-editor [hardcodedDatasource]=\"tool.inputs['componentRef']?.value?.component\"\n [saveCallback]=\"metaEditorSave\"></wuic-metadata-editor>\n }\n @if (toolProp.value.type == 'boolean') {\n <input type=\"checkbox\" [(ngModel)]=\"tool.inputs[toolProp.key]\" />\n }\n @if (toolProp.value.type == 'numberToArray') {\n <input style=\"width: 100%;\" type=\"number\" pInputText [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'txt_area') {\n <textarea style=\"width: 100%;\" [(ngModel)]=\"tool.inputs[toolProp.key]\"\n [ngModelOptions]=\"{ updateOn: 'blur' }\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\"></textarea>\n }\n @if (toolProp.value.type == 'string') {\n <input style=\"width: 100%;\" type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'color') {\n <p-colorPicker [ngModel]=\"getDesignerColorPickerHex(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorPickerHexChange($event, tool, toolProp)\" appendTo=\"body\" />\n <div class=\"designer-color-alpha-row\">\n <span class=\"designer-color-alpha-label\">{{ 'designer.alpha' | translate }}</span>\n <input type=\"range\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n <input type=\"number\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n </div>\n <input type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'dictionary' && !toolProp.value.async) {\n @if (toolProp.key == 'cssFile') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"availableProjectCssFiles\"\n [ngModel]=\"tool.inputs[toolProp.key]\" (ngModelChange)=\"onCssFileChanged($event, tool)\" />\n } @else if (toolProp.key == 'cssClass') {\n <p-multiSelect [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getCssClassOptionsForTool(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"onCssClassChanged($event, tool)\" [filter]=\"true\" [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"38\" scrollHeight=\"260px\" [showToggleAll]=\"false\" display=\"chip\"\n [maxSelectedLabels]=\"3\" [selectedItemsLabel]=\"'{0} selected'\">\n </p-multiSelect>\n } @else if ((tool.name == 'SELECT' || tool.name == 'MULTISELECT' || tool.name == 'UL') &&\n (toolProp.key == 'valueField' || toolProp.key == 'textField')) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else if (tool.name == 'LABEL' && toolProp.key == 'displayField') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n }\n @if (toolProp.value.type == 'dictionary' && toolProp.value.async) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (tool.name == 'DATAREPEATER' && toolProp.key == 'action' &&\n canOpenArchetypeConfigurator(tool)) {\n <button style=\"margin-top: 8px;\" (click)=\"openArchetypeConfigurator(tool)\">\n {{ 'designer.configure' | translate }} {{ (tool.inputs['action'] | async) || '' }}\n </button>\n }\n @if (toolProp.value.type == 'dropped-component-list') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [placeholder]=\"'select.component' | translate\" [options]=\"elementFilter(toolProp)\"\n [ngModel]=\"getDroppedComponentListModel(tool, toolProp)\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" optionLabel=\"uniqueName\"\n dataKey=\"uniqueName\" />\n }\n @if (toolProp.value.type == 'autocomplete') {\n <p-autoComplete [style]=\"{width: '100%'}\" appendTo=\"body\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\" [forceSelection]=\"true\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" [suggestions]=\"tool.suggestions\"\n [optionLabel]=\"toolProp.value.displayField\" [optionValue]=\"toolProp.value.valueField\"\n (completeMethod)=\"search($event, tool, toolProp)\" [dropdown]=\"true\" dropdownMode=\"current\"\n [multiple]=\"toolProp.value.multiple\" />\n }\n @if (toolProp.value.type == 'propertyTree' && shouldShowPropertyTree(tool)) {\n <ul>\n @for (item of $any(tool.inputs[toolProp.key] | async); track item) {\n <li style=\"padding: 0; margin: 0;\">\n @if (item.col && tool.inputs['customProps_' + (tool.inputs['action'] | async)])\n {\n <wuic-field-editor [field]=\"item.col\"\n [record]=\"tool.inputs['customProps_' + (tool.inputs['action'] | async)]\"\n [metaInfo]=\"tool.inputs['datasource']?.component?.value?.metaInfo\"\n [triggerProp]=\"tool.inputs['action']\"></wuic-field-editor>\n }\n </li>\n }\n </ul>\n }\n </li>\n }\n </ul>\n </details>\n }\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"hierarchy-panel\">\n <div class=\"hierarchy-panel-title\">{{ 'designer.canvas_hierarchy' | translate }}</div>\n <div class=\"hierarchy-tree-wrap\">\n <p-tree [value]=\"hierarchyTreeNodes\" [draggableNodes]=\"true\" [droppableNodes]=\"true\"\n selectionMode=\"single\" [(selection)]=\"hierarchySelection\"\n (onNodeSelect)=\"onHierarchyNodeSelect($event)\" draggableScope=\"designer-hierarchy\"\n droppableScope=\"designer-hierarchy\" (onNodeDrop)=\"onHierarchyNodeDrop($event)\">\n <ng-template let-node pTemplate=\"default\">\n <span class=\"hierarchy-node-label\" [class.active]=\"isHierarchyNodeSelected(node)\"\n (mouseenter)=\"onHierarchyNodeMouseEnter(node)\" (mouseleave)=\"onHierarchyNodeMouseLeave()\"\n (contextmenu)=\"showHierarchyNodeContextMenu($event, node, hierarchyCtxMenu)\">\n {{ node.label }}\n </span>\n </ng-template>\n </p-tree>\n </div>\n <p-contextMenu #hierarchyCtxMenu [model]=\"hierarchyCtxItems\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </ng-template>\n </p-splitter>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"col dashboard-design\" [class.designer-hide-datasources]=\"hideDatasourcesInCanvas\" pDroppable\n (onDrop)=\"drop($event)\" #canvas (contextmenu)=\"showCtx($event, this)\">\n <wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n <p-contextMenu [target]=\"canvas\" [model]=\"ctxItems | async\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </div>\n <div class=\"designer-footer\">\n <div class=\"designer-footer-label\">{{ 'designer.items' | translate }}:</div>\n <div class=\"designer-footer-list\">\n @for (item of droppedDashboardItems; track item.uniqueName) {\n <button type=\"button\" class=\"designer-footer-item\" [class.active]=\"tool?.uniqueName === item?.uniqueName\"\n [attr.data-designer-unique-name]=\"item?.uniqueName\" (mouseenter)=\"setFooterHoverHighlight(item)\"\n (mouseleave)=\"clearFooterHoverHighlight()\" (click)=\"selectDashboardItem(item)\"\n (contextmenu)=\"showFooterContextMenu($event, item, footerCtxMenu)\">\n {{ getDroppedDashboardItemLabel(item) }}\n </button>\n }\n </div>\n <p-contextMenu #footerCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n}\n\n@if (noEdit) {\n<wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n}\n\n<wuic-archetype-configurator [visible]=\"showArchetypeConfigurator\"\n (visibleChange)=\"onArchetypeConfigVisibleChange($event)\" [archetype]=\"getActiveArchetypeConfig()\"\n [metaInfo]=\"getArchetypeConfigMetaInfoForTool(archetypeConfigTool)\"\n [value]=\"getArchetypeConfigValueForTool(archetypeConfigTool)\" (applyConfig)=\"onArchetypeConfigApply($event)\">\n</wuic-archetype-configurator>\n\n<wuic-css-sheet-editor [visible]=\"showCssSheetEditor\" (visibleChange)=\"onCssSheetEditorVisibleChange($event)\"\n [sheets]=\"dashboardCssSheets\" [selectedSheetPath]=\"selectedCssEditorSheetPath\" [assetsFolder]=\"currentDashboardRoute\"\n [styleProps]=\"getCssEditorStyleProps()\" (applyEditor)=\"onCssSheetEditorApply($event)\">\n</wuic-css-sheet-editor>\n\n<p-dialog [header]=\"'designer.reopen_dashboard' | translate\" [(visible)]=\"reopenDashboardDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenDashboardDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"designer-reopen-select\">{{ 'designer.existing_dashboards' | translate }}</label>\n <p-select inputId=\"designer-reopen-select\" [options]=\"savedDashboardOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedDashboardRoute\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenDashboardDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenDashboardDialog()\"\n [disabled]=\"!reopenDialogSelectedDashboardRoute\">{{ 'designer.reopen' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;width:100%;height:100%;min-width:0}.designer-layout{display:grid;grid-template-rows:auto minmax(0,1fr) auto;height:100%;min-height:0;width:100%;min-width:0}.designer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;border:1px solid inherit;border-radius:10px;background:transparent;padding:10px 12px;margin-bottom:8px}.designer-current-dashboard{min-width:220px;font-size:.86rem;font-weight:700;color:inherit}.designer-toolbar-spacer{flex:1 1 auto;min-width:16px}.designer-toolbar-menu-group{display:inline-flex}.designer-toolbar-menu-group button{height:32px;border:1px solid #2f67d8;border-radius:6px;background:#2f67d8;color:#fff;padding:0 10px;font-size:.82rem;cursor:pointer}.designer-toolbar-menu-group button[disabled]{opacity:.6;cursor:not-allowed}.designer-main{min-height:0;min-width:0;width:100%;overflow:hidden;direction:ltr}.designer-main-splitter{height:100%;width:100%;min-width:0;direction:ltr}.designer-toolbox{font-size:3rem}.designer-toolbox ul{list-style-type:none;padding:0;margin:0;width:100%}.designer-toolbox li{float:left;margin-left:5px;margin-top:5px}.designer-toolbox .tool-groups{display:block}.designer-toolbox .tool-group-item{float:none;margin-left:0;margin-top:10px;clear:both}.designer-toolbox .tool-group-title{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.7;margin:0 0 4px 6px}.designer-toolbox .tool-group-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}.designer-toolbox .tool-group-list li{float:none;margin-left:0;margin-top:0}.designer-toolbox .props{font-size:1rem}.designer-toolbox .props li{float:none;padding-bottom:15px}.designer-toolbox .prop-section{width:100%;border:1px solid inherit;border-radius:6px;margin-top:8px;background:transparent;padding:6px 8px 2px}.designer-toolbox .prop-section-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;color:inherit;margin-bottom:6px}.palette-panel{height:100%;overflow:auto;width:100%}.designer-color-alpha-row{display:flex;align-items:center;gap:8px;margin-top:6px;margin-bottom:6px}.designer-color-alpha-label{font-size:.8rem;min-width:40px}.designer-color-alpha-row input[type=range]{flex:1 1 auto;min-width:90px}.designer-color-alpha-row input[type=number]{width:72px}.properties-panel{height:100%;overflow:hidden;width:100%;display:flex;flex-direction:column;min-height:0;padding-right:10px;padding-left:10px;padding-top:10px}.properties-panel>.designer-toolbox{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable}:host ::ng-deep .designer-left-bottom-splitter .p-splitterpanel{min-height:0!important;overflow:hidden!important}.hierarchy-panel{height:100%;width:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding:6px}.hierarchy-panel-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#4b5563;margin:0 0 6px}.hierarchy-tree-wrap{flex:1 1 auto;min-height:0;border:1px solid inherit;border-radius:6px;background:transparent;overflow:auto;padding:4px}:host ::ng-deep .hierarchy-tree-wrap .p-tree{border:none;padding:0;height:100%;background:transparent}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content{border:1px solid transparent;border-radius:4px;background:transparent!important;color:inherit!important;padding:0!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight{border-color:transparent!important;background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content:hover{background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label{display:inline-block;border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label:hover{border-color:#94a3b8}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label.active{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight .hierarchy-node-label{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}.designer-hierarchy-node{cursor:pointer;border:1px solid transparent;border-radius:4px;padding:2px 6px;font-size:.82rem;line-height:1.25rem}.designer-hierarchy-node-label{cursor:pointer;display:inline-block;width:100%}.designer-hierarchy-node.active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.designer-footer-item.designer-tree-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px}.dashboard-design{height:100%;min-height:0;min-width:0;width:100%;overflow:auto;direction:ltr}:host ::ng-deep .dashboard-design.designer-hide-datasources .datasource-descriptor{display:none!important}.current-dashboard-title{font-weight:600;margin-right:12px}.designer-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid inherit;background:transparent;min-height:36px;flex:0 0 auto;width:100%;overflow:auto}.designer-footer-label{font-size:.85rem;font-weight:600;color:inherit;white-space:nowrap}.designer-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.designer-footer-item{border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap}.designer-footer-item.active{border-color:#3b82f6;background:transparent;color:#1d4ed8}:host ::ng-deep .designer-footer-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px;box-shadow:inset 0 0 0 1px #3b82f6!important}.route-popup-form{display:flex;flex-direction:column;gap:8px}.route-popup-form label{font-size:.85rem;font-weight:600;color:#2f3f58}.route-popup-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.route-popup-btn{border:1px solid inherit;background:transparent;color:inherit;border-radius:6px;padding:7px 12px;font-size:.85rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f67d8;background:#2f67d8;color:#fff}.p-colorpicker{border:1px solid black;border-radius:4px}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TranslateModule }, { 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i12.Draggable, selector: "[pDraggable]", inputs: ["pDraggable", "dragEffect", "dragHandle", "pDraggableDisabled"], outputs: ["onDragStart", "onDragEnd", "onDrag"] }, { kind: "directive", type: i12.Droppable, selector: "[pDroppable]", inputs: ["pDroppable", "pDroppableDisabled", "dropEffect"], outputs: ["onDragEnter", "onDragLeave", "onDrop"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: ColorPickerModule }, { kind: "component", type: i2$1.ColorPicker, selector: "p-colorPicker, p-colorpicker, p-color-picker", inputs: ["styleClass", "showTransitionOptions", "hideTransitionOptions", "inline", "format", "tabindex", "inputId", "autoZIndex", "autofocus", "defaultColor", "appendTo", "overlayOptions", "motionOptions"], outputs: ["onChange", "onShow", "onHide"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2$2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "component", type: DashboardComponent, selector: "wuic-dashboard", inputs: ["dashboardElements"] }, { kind: "component", type: FieldEditorComponent, selector: "wuic-field-editor", inputs: ["datasource", "record", "field", "metaInfo", "readOnly", "isFilter", "hideLabel", "forceShowLabel", "operator", "nestedIndex", "triggerProp", "tabIndex", "onInlineCellValueChange"] }, { kind: "ngmodule", type: ContextMenuModule }, { kind: "component", type: i14.ContextMenu, selector: "p-contextMenu, p-contextmenu, p-context-menu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "autoZIndex", "baseZIndex", "id", "breakpoint", "ariaLabel", "ariaLabelledBy", "pressDelay", "appendTo", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: MetadataEditorComponent, selector: "wuic-metadata-editor", inputs: ["datasource", "hardcodedDatasource", "saveCallback", "hideReports", "hideRelatedTableActions", "hideRelatedColumnActions"] }, { kind: "ngmodule", type: MenubarModule }, { kind: "component", type: ArchetypeConfiguratorComponent, selector: "wuic-archetype-configurator", inputs: ["visible", "archetype", "metaInfo", "value"], outputs: ["visibleChange", "applyConfig"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i18.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "component", type: CssSheetEditorComponent, selector: "wuic-css-sheet-editor", inputs: ["visible", "sheets", "selectedSheetPath", "styleProps", "assetsFolder"], outputs: ["visibleChange", "applyEditor"] }, { kind: "ngmodule", type: MenuModule }, { kind: "component", type: i9.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex", "appendTo", "motionOptions"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TreeModule }, { kind: "component", type: i4$1.Tree, selector: "p-tree", inputs: ["value", "selectionMode", "loadingMode", "selection", "styleClass", "contextMenu", "contextMenuSelectionMode", "contextMenuSelection", "draggableScope", "droppableScope", "draggableNodes", "droppableNodes", "metaKeySelection", "propagateSelectionUp", "propagateSelectionDown", "loading", "loadingIcon", "emptyMessage", "ariaLabel", "togglerAriaLabel", "ariaLabelledBy", "validateDrop", "filter", "filterInputAutoFocus", "filterBy", "filterMode", "filterOptions", "filterPlaceholder", "filteredNodes", "filterLocale", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "indentation", "_templateMap", "trackBy", "highlightOnSelect"], outputs: ["selectionChange", "contextMenuSelectionChange", "onNodeSelect", "onNodeUnselect", "onNodeExpand", "onNodeCollapse", "onNodeContextMenuSelect", "onNodeDoubleClick", "onNodeDrop", "onLazyLoad", "onScroll", "onScrollIndexChange", "onFilter"] }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
13293
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DesignerComponent, isStandalone: true, selector: "wuic-designer", host: { listeners: { "window:hashchange": "onWindowHashChange()", "window:beforeunload": "onWindowBeforeUnload($event)", "window:keydown": "onWindowKeydown($event)", "window:mouseup": "onWindowMouseup()", "window:touchend": "onWindowTouchend()", "window:keyup": "onWindowKeyup()", "window:change": "onWindowChange()" } }, providers: [TreeDragDropService], viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }], ngImport: i0, template: "<!-- <button pButton type=\"button\" (click)=\"loadDashboard()\" class=\"mr-2\">{{'load' | translate}}</button> -->\n@if (!noEdit) {\n<div class=\"designer-layout\">\n <div class=\"designer-toolbar\">\n <div class=\"designer-current-dashboard\">{{ currentDashboardTitle }}</div>\n\n <div class=\"designer-toolbar-spacer\"></div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"undo()\" [disabled]=\"!canUndo\">{{ 'designer.undo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"redo()\" [disabled]=\"!canRedo\">{{ 'designer.redo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" data-testid=\"designer-actions-menu-btn\" (click)=\"refreshGraphActionsMenuItems(); graphActionsMenu.toggle($event)\">{{ 'designer.actions_menu' | translate }}</button>\n <p-menu #graphActionsMenu [popup]=\"true\" [model]=\"graphActionsMenuItems\" appendTo=\"body\"></p-menu>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"openCurrentDashboardInNewTab()\" [disabled]=\"!currentDashboardRoute\">{{\n 'designer.open_current_dashboard' | translate }}</button>\n </div>\n </div>\n <div class=\"designer-main\">\n <p-splitter [style]=\"{ height: '100%' }\" [panelSizes]=\"[15, 85]\" styleClass=\"designer-main-splitter\">\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[35, 65]\"\n [minSizes]=\"[20, 30]\" styleClass=\"designer-left-splitter\">\n <ng-template pTemplate>\n <div class=\"col flex designer-toolbox palette-panel\">\n <ul class=\"tool-groups\">\n @for (toolGroup of availableToolGroups; track toolGroup.group) {\n <li class=\"tool-group-item\">\n <div class=\"tool-group-title\">{{ toolGroup.group }}</div>\n <ul class=\"tool-group-list\">\n @for (tool of toolGroup.tools; track tool) {\n <li pDraggable (onDragStart)=\"dragStart(tool)\" (onDragEnd)=\"dragEnd()\" (onDrag)=\"onDrag($event)\"\n (dblclick)=\"dropToolAtRoot(tool)\">\n <i [ngClass]=\"tool.icon\" [title]=\"tool.name\"></i>\n </li>\n }\n </ul>\n </li>\n }\n </ul>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[68, 32]\"\n [minSizes]=\"[35, 20]\" styleClass=\"designer-left-bottom-splitter\">\n <ng-template pTemplate>\n <div class=\"properties-panel\">\n <p-select [style]=\"{width: '100%'}\" [options]=\"flattenedDashboardElements\" [(ngModel)]=\"tool\"\n optionLabel=\"uniqueName\" dataKey=\"componentId\" appendTo=\"body\" />\n <div class=\"designer-toolbox\">\n\n <span style=\"width: 100%;\">\n <input type=\"text\" style=\"width: calc(100% - 30px);\" [(ngModel)]=\"propFilter\"\n [placeholder]=\"'prop.filter' | translate\" />\n <button style=\"width: 25px\" (click)=\"clearPropFilter()\">X</button>\n </span>\n\n @for (section of getToolPropertySections(tool, propFilter); track section.key) {\n <details class=\"prop-section\" [open]=\"section.open\"\n (toggle)=\"onPropertySectionToggle(tool, section.key, $event)\">\n <summary class=\"prop-section-title\">{{ section.label }}</summary>\n <ul class=\"props\">\n @for (toolProp of section.props; track toolProp.key) {\n <li>\n @if (!toolProp.value.hideCaption) {\n <span>{{ toolProp.value?.propertyCaption ||\n toolProp.key[0].toUpperCase() + toolProp.key.slice(1)\n }}</span>\n }\n <br />\n @if (toolProp.value.type == 'button') {\n <p-button (click)=\"toolProp.value.callback(tool.inputs, toolProp.key, null, tool)\"\n class=\"k-button\" [style]=\"{width: '100%'}\">\n {{toolProp.value.propertyCaption}}\n </p-button>\n }\n @if (toolProp.value.type == 'metaEditor' &&\n tool.inputs['componentRef']?.value?.component) {\n <wuic-metadata-editor [hardcodedDatasource]=\"tool.inputs['componentRef']?.value?.component\"\n [saveCallback]=\"metaEditorSave\"></wuic-metadata-editor>\n }\n @if (toolProp.value.type == 'boolean') {\n <input type=\"checkbox\" [(ngModel)]=\"tool.inputs[toolProp.key]\" />\n }\n @if (toolProp.value.type == 'numberToArray') {\n <input style=\"width: 100%;\" type=\"number\" pInputText [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'txt_area') {\n <textarea style=\"width: 100%;\" [(ngModel)]=\"tool.inputs[toolProp.key]\"\n [ngModelOptions]=\"{ updateOn: 'blur' }\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\"></textarea>\n }\n @if (toolProp.value.type == 'string') {\n <input style=\"width: 100%;\" type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'color') {\n <p-colorPicker [ngModel]=\"getDesignerColorPickerHex(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorPickerHexChange($event, tool, toolProp)\" appendTo=\"body\" />\n <div class=\"designer-color-alpha-row\">\n <span class=\"designer-color-alpha-label\">{{ 'designer.alpha' | translate }}</span>\n <input type=\"range\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n <input type=\"number\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n </div>\n <input type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'dictionary' && !toolProp.value.async) {\n @if (toolProp.key == 'cssFile') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"availableProjectCssFiles\"\n [ngModel]=\"tool.inputs[toolProp.key]\" (ngModelChange)=\"onCssFileChanged($event, tool)\" />\n } @else if (toolProp.key == 'cssClass') {\n <p-multiSelect [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getCssClassOptionsForTool(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"onCssClassChanged($event, tool)\" [filter]=\"true\" [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"38\" scrollHeight=\"260px\" [showToggleAll]=\"false\" display=\"chip\"\n [maxSelectedLabels]=\"3\" [selectedItemsLabel]=\"'{0} selected'\">\n </p-multiSelect>\n } @else if ((tool.name == 'SELECT' || tool.name == 'MULTISELECT' || tool.name == 'UL') &&\n (toolProp.key == 'valueField' || toolProp.key == 'textField')) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else if (tool.name == 'LABEL' && toolProp.key == 'displayField') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n }\n @if (toolProp.value.type == 'dictionary' && toolProp.value.async) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (tool.name == 'DATAREPEATER' && toolProp.key == 'action' &&\n canOpenArchetypeConfigurator(tool)) {\n <button style=\"margin-top: 8px;\" (click)=\"openArchetypeConfigurator(tool)\">\n {{ 'designer.configure' | translate }} {{ (tool.inputs['action'] | async) || '' }}\n </button>\n }\n @if (toolProp.value.type == 'dropped-component-list') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [placeholder]=\"'select.component' | translate\" [options]=\"elementFilter(toolProp)\"\n [ngModel]=\"getDroppedComponentListModel(tool, toolProp)\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" optionLabel=\"uniqueName\"\n dataKey=\"uniqueName\" />\n }\n @if (toolProp.value.type == 'autocomplete') {\n <p-autoComplete [style]=\"{width: '100%'}\" appendTo=\"body\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\" [forceSelection]=\"true\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" [suggestions]=\"tool.suggestions\"\n [optionLabel]=\"toolProp.value.displayField\" [optionValue]=\"toolProp.value.valueField\"\n (completeMethod)=\"search($event, tool, toolProp)\" [dropdown]=\"true\" dropdownMode=\"current\"\n [multiple]=\"toolProp.value.multiple\" />\n }\n @if (toolProp.value.type == 'propertyTree' && shouldShowPropertyTree(tool)) {\n <ul>\n @for (item of $any(tool.inputs[toolProp.key] | async); track item) {\n <li style=\"padding: 0; margin: 0;\">\n @if (item.col && tool.inputs['customProps_' + (tool.inputs['action'] | async)])\n {\n <wuic-field-editor [field]=\"item.col\"\n [record]=\"tool.inputs['customProps_' + (tool.inputs['action'] | async)]\"\n [metaInfo]=\"tool.inputs['datasource']?.component?.value?.metaInfo\"\n [triggerProp]=\"tool.inputs['action']\"></wuic-field-editor>\n }\n </li>\n }\n </ul>\n }\n </li>\n }\n </ul>\n </details>\n }\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"hierarchy-panel\">\n <div class=\"hierarchy-panel-title\">{{ 'designer.canvas_hierarchy' | translate }}</div>\n <div class=\"hierarchy-tree-wrap\">\n <p-tree [value]=\"hierarchyTreeNodes\" [draggableNodes]=\"true\" [droppableNodes]=\"true\"\n selectionMode=\"single\" [(selection)]=\"hierarchySelection\"\n (onNodeSelect)=\"onHierarchyNodeSelect($event)\" draggableScope=\"designer-hierarchy\"\n droppableScope=\"designer-hierarchy\" (onNodeDrop)=\"onHierarchyNodeDrop($event)\">\n <ng-template let-node pTemplate=\"default\">\n <span class=\"hierarchy-node-label\" [class.active]=\"isHierarchyNodeSelected(node)\"\n (mouseenter)=\"onHierarchyNodeMouseEnter(node)\" (mouseleave)=\"onHierarchyNodeMouseLeave()\"\n (contextmenu)=\"showHierarchyNodeContextMenu($event, node, hierarchyCtxMenu)\">\n {{ node.label }}\n </span>\n </ng-template>\n </p-tree>\n </div>\n <p-contextMenu #hierarchyCtxMenu [model]=\"hierarchyCtxItems\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </ng-template>\n </p-splitter>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"col dashboard-design\" [class.designer-hide-datasources]=\"hideDatasourcesInCanvas\"\n [class.designer-snap-grid]=\"snapToGridEnabled\" [style.--wuic-grid-size.px]=\"gridSize\" pDroppable\n (onDrop)=\"drop($event)\" #canvas (mousedown)=\"onCanvasMouseDown($event)\"\n (contextmenu)=\"showCtx($event, this)\">\n <wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n @if (rubberBandVisible) {\n <div class=\"designer-selection-rect\" [style.left]=\"rubberBandStyle.left\" [style.top]=\"rubberBandStyle.top\"\n [style.width]=\"rubberBandStyle.width\" [style.height]=\"rubberBandStyle.height\"></div>\n }\n <p-contextMenu [target]=\"canvas\" [model]=\"ctxItems | async\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </div>\n <div class=\"designer-footer\">\n <div class=\"designer-footer-label\">{{ 'designer.items' | translate }}:</div>\n <div class=\"designer-footer-list\">\n @for (item of droppedDashboardItems; track item.uniqueName) {\n <button type=\"button\" class=\"designer-footer-item\" [class.active]=\"tool?.uniqueName === item?.uniqueName\"\n [attr.data-designer-unique-name]=\"item?.uniqueName\" (mouseenter)=\"setFooterHoverHighlight(item)\"\n (mouseleave)=\"clearFooterHoverHighlight()\" (click)=\"selectDashboardItem(item)\"\n (contextmenu)=\"showFooterContextMenu($event, item, footerCtxMenu)\">\n {{ getDroppedDashboardItemLabel(item) }}\n </button>\n }\n </div>\n <p-contextMenu #footerCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n}\n\n@if (noEdit) {\n<wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n}\n\n<wuic-archetype-configurator [visible]=\"showArchetypeConfigurator\"\n (visibleChange)=\"onArchetypeConfigVisibleChange($event)\" [archetype]=\"getActiveArchetypeConfig()\"\n [metaInfo]=\"getArchetypeConfigMetaInfoForTool(archetypeConfigTool)\"\n [value]=\"getArchetypeConfigValueForTool(archetypeConfigTool)\" (applyConfig)=\"onArchetypeConfigApply($event)\">\n</wuic-archetype-configurator>\n\n<wuic-css-sheet-editor [visible]=\"showCssSheetEditor\" (visibleChange)=\"onCssSheetEditorVisibleChange($event)\"\n [sheets]=\"dashboardCssSheets\" [selectedSheetPath]=\"selectedCssEditorSheetPath\" [assetsFolder]=\"currentDashboardRoute\"\n [styleProps]=\"getCssEditorStyleProps()\" (applyEditor)=\"onCssSheetEditorApply($event)\">\n</wuic-css-sheet-editor>\n\n<p-dialog [header]=\"'designer.reopen_dashboard' | translate\" [(visible)]=\"reopenDashboardDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenDashboardDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"designer-reopen-select\">{{ 'designer.existing_dashboards' | translate }}</label>\n <p-select inputId=\"designer-reopen-select\" [options]=\"savedDashboardOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedDashboardRoute\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenDashboardDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenDashboardDialog()\"\n [disabled]=\"!reopenDialogSelectedDashboardRoute\">{{ 'designer.reopen' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;width:100%;height:100%;min-width:0}.designer-layout{display:grid;grid-template-rows:auto minmax(0,1fr) auto;height:100%;min-height:0;width:100%;min-width:0}.designer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;border:1px solid inherit;border-radius:10px;background:transparent;padding:10px 12px;margin-bottom:8px}.designer-current-dashboard{min-width:220px;font-size:.86rem;font-weight:700;color:inherit}.designer-toolbar-spacer{flex:1 1 auto;min-width:16px}.designer-toolbar-menu-group{display:inline-flex}.designer-toolbar-menu-group button{height:32px;border:1px solid #2f67d8;border-radius:6px;background:#2f67d8;color:#fff;padding:0 10px;font-size:.82rem;cursor:pointer}.designer-toolbar-menu-group button[disabled]{opacity:.6;cursor:not-allowed}.designer-main{min-height:0;min-width:0;width:100%;overflow:hidden;direction:ltr}.designer-main-splitter{height:100%;width:100%;min-width:0;direction:ltr}.designer-toolbox{font-size:3rem}.designer-toolbox ul{list-style-type:none;padding:0;margin:0;width:100%}.designer-toolbox li{float:left;margin-left:5px;margin-top:5px}.designer-toolbox .tool-groups{display:block}.designer-toolbox .tool-group-item{float:none;margin-left:0;margin-top:10px;clear:both}.designer-toolbox .tool-group-title{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.7;margin:0 0 4px 6px}.designer-toolbox .tool-group-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}.designer-toolbox .tool-group-list li{float:none;margin-left:0;margin-top:0}.designer-toolbox .props{font-size:1rem}.designer-toolbox .props li{float:none;padding-bottom:15px}.designer-toolbox .prop-section{width:100%;border:1px solid inherit;border-radius:6px;margin-top:8px;background:transparent;padding:6px 8px 2px}.designer-toolbox .prop-section-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;color:inherit;margin-bottom:6px}.palette-panel{height:100%;overflow:auto;width:100%}.designer-color-alpha-row{display:flex;align-items:center;gap:8px;margin-top:6px;margin-bottom:6px}.designer-color-alpha-label{font-size:.8rem;min-width:40px}.designer-color-alpha-row input[type=range]{flex:1 1 auto;min-width:90px}.designer-color-alpha-row input[type=number]{width:72px}.properties-panel{height:100%;overflow:hidden;width:100%;display:flex;flex-direction:column;min-height:0;padding-right:10px;padding-left:10px;padding-top:10px}.properties-panel>.designer-toolbox{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable}:host ::ng-deep .designer-left-bottom-splitter .p-splitterpanel{min-height:0!important;overflow:hidden!important}.hierarchy-panel{height:100%;width:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding:6px}.hierarchy-panel-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#4b5563;margin:0 0 6px}.hierarchy-tree-wrap{flex:1 1 auto;min-height:0;border:1px solid inherit;border-radius:6px;background:transparent;overflow:auto;padding:4px}:host ::ng-deep .hierarchy-tree-wrap .p-tree{border:none;padding:0;height:100%;background:transparent}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content{border:1px solid transparent;border-radius:4px;background:transparent!important;color:inherit!important;padding:0!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight{border-color:transparent!important;background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content:hover{background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label{display:inline-block;border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label:hover{border-color:#94a3b8}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label.active{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight .hierarchy-node-label{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}.designer-hierarchy-node{cursor:pointer;border:1px solid transparent;border-radius:4px;padding:2px 6px;font-size:.82rem;line-height:1.25rem}.designer-hierarchy-node-label{cursor:pointer;display:inline-block;width:100%}.designer-hierarchy-node.active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.designer-footer-item.designer-tree-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px}.dashboard-design{height:100%;min-height:0;min-width:0;width:100%;overflow:auto;direction:ltr;position:relative}.designer-selection-rect{position:absolute;border:1px dashed #3b82f6;background:#3b82f61f;pointer-events:none;z-index:10000}:host ::ng-deep .designer-multi-selected{outline:2px solid #3b82f6!important;outline-offset:1px}:host ::ng-deep .dashboard-design.designer-hide-datasources .datasource-descriptor{display:none!important}.current-dashboard-title{font-weight:600;margin-right:12px}.designer-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid inherit;background:transparent;min-height:36px;flex:0 0 auto;width:100%;overflow:auto}.designer-footer-label{font-size:.85rem;font-weight:600;color:inherit;white-space:nowrap}.designer-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.designer-footer-item{border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap}.designer-footer-item.active{border-color:#3b82f6;background:transparent;color:#1d4ed8}:host ::ng-deep .designer-footer-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px;box-shadow:inset 0 0 0 1px #3b82f6!important}.route-popup-form{display:flex;flex-direction:column;gap:8px}.route-popup-form label{font-size:.85rem;font-weight:600;color:#2f3f58}.route-popup-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.route-popup-btn{border:1px solid inherit;background:transparent;color:inherit;border-radius:6px;padding:7px 12px;font-size:.85rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f67d8;background:#2f67d8;color:#fff}.p-colorpicker{border:1px solid black;border-radius:4px}.dashboard-design.designer-snap-grid{background-image:linear-gradient(to right,rgba(128,128,128,.16) 1px,transparent 1px),linear-gradient(to bottom,rgba(128,128,128,.16) 1px,transparent 1px);background-size:var(--wuic-grid-size, 10px) var(--wuic-grid-size, 10px)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TranslateModule }, { 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i12.Draggable, selector: "[pDraggable]", inputs: ["pDraggable", "dragEffect", "dragHandle", "pDraggableDisabled"], outputs: ["onDragStart", "onDragEnd", "onDrag"] }, { kind: "directive", type: i12.Droppable, selector: "[pDroppable]", inputs: ["pDroppable", "pDroppableDisabled", "dropEffect"], outputs: ["onDragEnter", "onDragLeave", "onDrop"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: ColorPickerModule }, { kind: "component", type: i2$1.ColorPicker, selector: "p-colorPicker, p-colorpicker, p-color-picker", inputs: ["styleClass", "showTransitionOptions", "hideTransitionOptions", "inline", "format", "tabindex", "inputId", "autoZIndex", "autofocus", "defaultColor", "appendTo", "overlayOptions", "motionOptions"], outputs: ["onChange", "onShow", "onHide"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2$2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "component", type: DashboardComponent, selector: "wuic-dashboard", inputs: ["dashboardElements"] }, { kind: "component", type: FieldEditorComponent, selector: "wuic-field-editor", inputs: ["datasource", "record", "field", "metaInfo", "readOnly", "isFilter", "hideLabel", "forceShowLabel", "operator", "nestedIndex", "triggerProp", "tabIndex", "onInlineCellValueChange"] }, { kind: "ngmodule", type: ContextMenuModule }, { kind: "component", type: i10$1.ContextMenu, selector: "p-contextMenu, p-contextmenu, p-context-menu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "autoZIndex", "baseZIndex", "id", "breakpoint", "ariaLabel", "ariaLabelledBy", "pressDelay", "appendTo", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: MetadataEditorComponent, selector: "wuic-metadata-editor", inputs: ["datasource", "hardcodedDatasource", "saveCallback", "hideReports", "hideRelatedTableActions", "hideRelatedColumnActions"] }, { kind: "ngmodule", type: MenubarModule }, { kind: "component", type: ArchetypeConfiguratorComponent, selector: "wuic-archetype-configurator", inputs: ["visible", "archetype", "metaInfo", "value"], outputs: ["visibleChange", "applyConfig"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i18.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "component", type: CssSheetEditorComponent, selector: "wuic-css-sheet-editor", inputs: ["visible", "sheets", "selectedSheetPath", "styleProps", "assetsFolder"], outputs: ["visibleChange", "applyEditor"] }, { kind: "ngmodule", type: MenuModule }, { kind: "component", type: i7.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex", "appendTo", "motionOptions"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TreeModule }, { kind: "component", type: i4$1.Tree, selector: "p-tree", inputs: ["value", "selectionMode", "loadingMode", "selection", "styleClass", "contextMenu", "contextMenuSelectionMode", "contextMenuSelection", "draggableScope", "droppableScope", "draggableNodes", "droppableNodes", "metaKeySelection", "propagateSelectionUp", "propagateSelectionDown", "loading", "loadingIcon", "emptyMessage", "ariaLabel", "togglerAriaLabel", "ariaLabelledBy", "validateDrop", "filter", "filterInputAutoFocus", "filterBy", "filterMode", "filterOptions", "filterPlaceholder", "filteredNodes", "filterLocale", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "indentation", "_templateMap", "trackBy", "highlightOnSelect"], outputs: ["selectionChange", "contextMenuSelectionChange", "onNodeSelect", "onNodeUnselect", "onNodeExpand", "onNodeCollapse", "onNodeContextMenuSelect", "onNodeDoubleClick", "onNodeDrop", "onLazyLoad", "onScroll", "onScrollIndexChange", "onFilter"] }, { kind: "pipe", type: i2$3.TranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
11832
13294
  }
11833
13295
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DesignerComponent, decorators: [{
11834
13296
  type: Component,
11835
- args: [{ selector: 'wuic-designer', imports: [NgClass, TranslateModule, AsyncPipe, FormsModule, SplitterModule, DragDropModule, SelectModule, ColorPickerModule, AutoCompleteModule, DashboardComponent, FieldEditorComponent, ContextMenuModule, MetadataEditorComponent, MenubarModule, ArchetypeConfiguratorComponent, ButtonModule, MultiSelectModule, CssSheetEditorComponent, MenuModule, DialogModule, TreeModule], providers: [TreeDragDropService], template: "<!-- <button pButton type=\"button\" (click)=\"loadDashboard()\" class=\"mr-2\">{{'load' | translate}}</button> -->\n@if (!noEdit) {\n<div class=\"designer-layout\">\n <div class=\"designer-toolbar\">\n <div class=\"designer-current-dashboard\">{{ currentDashboardTitle }}</div>\n\n <div class=\"designer-toolbar-spacer\"></div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"undo()\" [disabled]=\"!canUndo\">{{ 'designer.undo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"redo()\" [disabled]=\"!canRedo\">{{ 'designer.redo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"refreshGraphActionsMenuItems(); graphActionsMenu.toggle($event)\">{{ 'designer.actions_menu' | translate }}</button>\n <p-menu #graphActionsMenu [popup]=\"true\" [model]=\"graphActionsMenuItems\" appendTo=\"body\"></p-menu>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"openCurrentDashboardInNewTab()\" [disabled]=\"!currentDashboardRoute\">{{\n 'designer.open_current_dashboard' | translate }}</button>\n </div>\n </div>\n <div class=\"designer-main\">\n <p-splitter [style]=\"{ height: '100%' }\" [panelSizes]=\"[15, 85]\" styleClass=\"designer-main-splitter\">\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[35, 65]\"\n [minSizes]=\"[20, 30]\" styleClass=\"designer-left-splitter\">\n <ng-template pTemplate>\n <div class=\"col flex designer-toolbox palette-panel\">\n <ul class=\"tool-groups\">\n @for (toolGroup of availableToolGroups; track toolGroup.group) {\n <li class=\"tool-group-item\">\n <div class=\"tool-group-title\">{{ toolGroup.group }}</div>\n <ul class=\"tool-group-list\">\n @for (tool of toolGroup.tools; track tool) {\n <li pDraggable (onDragStart)=\"dragStart(tool)\" (onDragEnd)=\"dragEnd()\" (onDrag)=\"onDrag($event)\"\n (dblclick)=\"dropToolAtRoot(tool)\">\n <i [ngClass]=\"tool.icon\" [title]=\"tool.name\"></i>\n </li>\n }\n </ul>\n </li>\n }\n </ul>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[68, 32]\"\n [minSizes]=\"[35, 20]\" styleClass=\"designer-left-bottom-splitter\">\n <ng-template pTemplate>\n <div class=\"properties-panel\">\n <p-select [style]=\"{width: '100%'}\" [options]=\"flattenedDashboardElements\" [(ngModel)]=\"tool\"\n optionLabel=\"uniqueName\" dataKey=\"componentId\" appendTo=\"body\" />\n <div class=\"designer-toolbox\">\n\n <span style=\"width: 100%;\">\n <input type=\"text\" style=\"width: calc(100% - 30px);\" [(ngModel)]=\"propFilter\"\n [placeholder]=\"'prop.filter' | translate\" />\n <button style=\"width: 25px\" (click)=\"clearPropFilter()\">X</button>\n </span>\n\n @for (section of getToolPropertySections(tool, propFilter); track section.key) {\n <details class=\"prop-section\" [open]=\"section.open\"\n (toggle)=\"onPropertySectionToggle(tool, section.key, $event)\">\n <summary class=\"prop-section-title\">{{ section.label }}</summary>\n <ul class=\"props\">\n @for (toolProp of section.props; track toolProp.key) {\n <li>\n @if (!toolProp.value.hideCaption) {\n <span>{{ toolProp.value?.propertyCaption ||\n toolProp.key[0].toUpperCase() + toolProp.key.slice(1)\n }}</span>\n }\n <br />\n @if (toolProp.value.type == 'button') {\n <p-button (click)=\"toolProp.value.callback(tool.inputs, toolProp.key, null, tool)\"\n class=\"k-button\" [style]=\"{width: '100%'}\">\n {{toolProp.value.propertyCaption}}\n </p-button>\n }\n @if (toolProp.value.type == 'metaEditor' &&\n tool.inputs['componentRef']?.value?.component) {\n <wuic-metadata-editor [hardcodedDatasource]=\"tool.inputs['componentRef']?.value?.component\"\n [saveCallback]=\"metaEditorSave\"></wuic-metadata-editor>\n }\n @if (toolProp.value.type == 'boolean') {\n <input type=\"checkbox\" [(ngModel)]=\"tool.inputs[toolProp.key]\" />\n }\n @if (toolProp.value.type == 'numberToArray') {\n <input style=\"width: 100%;\" type=\"number\" pInputText [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'txt_area') {\n <textarea style=\"width: 100%;\" [(ngModel)]=\"tool.inputs[toolProp.key]\"\n [ngModelOptions]=\"{ updateOn: 'blur' }\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\"></textarea>\n }\n @if (toolProp.value.type == 'string') {\n <input style=\"width: 100%;\" type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'color') {\n <p-colorPicker [ngModel]=\"getDesignerColorPickerHex(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorPickerHexChange($event, tool, toolProp)\" appendTo=\"body\" />\n <div class=\"designer-color-alpha-row\">\n <span class=\"designer-color-alpha-label\">{{ 'designer.alpha' | translate }}</span>\n <input type=\"range\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n <input type=\"number\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n </div>\n <input type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'dictionary' && !toolProp.value.async) {\n @if (toolProp.key == 'cssFile') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"availableProjectCssFiles\"\n [ngModel]=\"tool.inputs[toolProp.key]\" (ngModelChange)=\"onCssFileChanged($event, tool)\" />\n } @else if (toolProp.key == 'cssClass') {\n <p-multiSelect [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getCssClassOptionsForTool(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"onCssClassChanged($event, tool)\" [filter]=\"true\" [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"38\" scrollHeight=\"260px\" [showToggleAll]=\"false\" display=\"chip\"\n [maxSelectedLabels]=\"3\" [selectedItemsLabel]=\"'{0} selected'\">\n </p-multiSelect>\n } @else if ((tool.name == 'SELECT' || tool.name == 'MULTISELECT' || tool.name == 'UL') &&\n (toolProp.key == 'valueField' || toolProp.key == 'textField')) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else if (tool.name == 'LABEL' && toolProp.key == 'displayField') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n }\n @if (toolProp.value.type == 'dictionary' && toolProp.value.async) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (tool.name == 'DATAREPEATER' && toolProp.key == 'action' &&\n canOpenArchetypeConfigurator(tool)) {\n <button style=\"margin-top: 8px;\" (click)=\"openArchetypeConfigurator(tool)\">\n {{ 'designer.configure' | translate }} {{ (tool.inputs['action'] | async) || '' }}\n </button>\n }\n @if (toolProp.value.type == 'dropped-component-list') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [placeholder]=\"'select.component' | translate\" [options]=\"elementFilter(toolProp)\"\n [ngModel]=\"getDroppedComponentListModel(tool, toolProp)\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" optionLabel=\"uniqueName\"\n dataKey=\"uniqueName\" />\n }\n @if (toolProp.value.type == 'autocomplete') {\n <p-autoComplete [style]=\"{width: '100%'}\" appendTo=\"body\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\" [forceSelection]=\"true\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" [suggestions]=\"tool.suggestions\"\n [optionLabel]=\"toolProp.value.displayField\" [optionValue]=\"toolProp.value.valueField\"\n (completeMethod)=\"search($event, tool, toolProp)\" [dropdown]=\"true\" dropdownMode=\"current\"\n [multiple]=\"toolProp.value.multiple\" />\n }\n @if (toolProp.value.type == 'propertyTree' && shouldShowPropertyTree(tool)) {\n <ul>\n @for (item of $any(tool.inputs[toolProp.key] | async); track item) {\n <li style=\"padding: 0; margin: 0;\">\n @if (item.col && tool.inputs['customProps_' + (tool.inputs['action'] | async)])\n {\n <wuic-field-editor [field]=\"item.col\"\n [record]=\"tool.inputs['customProps_' + (tool.inputs['action'] | async)]\"\n [metaInfo]=\"tool.inputs['datasource']?.component?.value?.metaInfo\"\n [triggerProp]=\"tool.inputs['action']\"></wuic-field-editor>\n }\n </li>\n }\n </ul>\n }\n </li>\n }\n </ul>\n </details>\n }\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"hierarchy-panel\">\n <div class=\"hierarchy-panel-title\">{{ 'designer.canvas_hierarchy' | translate }}</div>\n <div class=\"hierarchy-tree-wrap\">\n <p-tree [value]=\"hierarchyTreeNodes\" [draggableNodes]=\"true\" [droppableNodes]=\"true\"\n selectionMode=\"single\" [(selection)]=\"hierarchySelection\"\n (onNodeSelect)=\"onHierarchyNodeSelect($event)\" draggableScope=\"designer-hierarchy\"\n droppableScope=\"designer-hierarchy\" (onNodeDrop)=\"onHierarchyNodeDrop($event)\">\n <ng-template let-node pTemplate=\"default\">\n <span class=\"hierarchy-node-label\" [class.active]=\"isHierarchyNodeSelected(node)\"\n (mouseenter)=\"onHierarchyNodeMouseEnter(node)\" (mouseleave)=\"onHierarchyNodeMouseLeave()\"\n (contextmenu)=\"showHierarchyNodeContextMenu($event, node, hierarchyCtxMenu)\">\n {{ node.label }}\n </span>\n </ng-template>\n </p-tree>\n </div>\n <p-contextMenu #hierarchyCtxMenu [model]=\"hierarchyCtxItems\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </ng-template>\n </p-splitter>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"col dashboard-design\" [class.designer-hide-datasources]=\"hideDatasourcesInCanvas\" pDroppable\n (onDrop)=\"drop($event)\" #canvas (contextmenu)=\"showCtx($event, this)\">\n <wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n <p-contextMenu [target]=\"canvas\" [model]=\"ctxItems | async\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </div>\n <div class=\"designer-footer\">\n <div class=\"designer-footer-label\">{{ 'designer.items' | translate }}:</div>\n <div class=\"designer-footer-list\">\n @for (item of droppedDashboardItems; track item.uniqueName) {\n <button type=\"button\" class=\"designer-footer-item\" [class.active]=\"tool?.uniqueName === item?.uniqueName\"\n [attr.data-designer-unique-name]=\"item?.uniqueName\" (mouseenter)=\"setFooterHoverHighlight(item)\"\n (mouseleave)=\"clearFooterHoverHighlight()\" (click)=\"selectDashboardItem(item)\"\n (contextmenu)=\"showFooterContextMenu($event, item, footerCtxMenu)\">\n {{ getDroppedDashboardItemLabel(item) }}\n </button>\n }\n </div>\n <p-contextMenu #footerCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n}\n\n@if (noEdit) {\n<wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n}\n\n<wuic-archetype-configurator [visible]=\"showArchetypeConfigurator\"\n (visibleChange)=\"onArchetypeConfigVisibleChange($event)\" [archetype]=\"getActiveArchetypeConfig()\"\n [metaInfo]=\"getArchetypeConfigMetaInfoForTool(archetypeConfigTool)\"\n [value]=\"getArchetypeConfigValueForTool(archetypeConfigTool)\" (applyConfig)=\"onArchetypeConfigApply($event)\">\n</wuic-archetype-configurator>\n\n<wuic-css-sheet-editor [visible]=\"showCssSheetEditor\" (visibleChange)=\"onCssSheetEditorVisibleChange($event)\"\n [sheets]=\"dashboardCssSheets\" [selectedSheetPath]=\"selectedCssEditorSheetPath\" [assetsFolder]=\"currentDashboardRoute\"\n [styleProps]=\"getCssEditorStyleProps()\" (applyEditor)=\"onCssSheetEditorApply($event)\">\n</wuic-css-sheet-editor>\n\n<p-dialog [header]=\"'designer.reopen_dashboard' | translate\" [(visible)]=\"reopenDashboardDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenDashboardDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"designer-reopen-select\">{{ 'designer.existing_dashboards' | translate }}</label>\n <p-select inputId=\"designer-reopen-select\" [options]=\"savedDashboardOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedDashboardRoute\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenDashboardDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenDashboardDialog()\"\n [disabled]=\"!reopenDialogSelectedDashboardRoute\">{{ 'designer.reopen' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;width:100%;height:100%;min-width:0}.designer-layout{display:grid;grid-template-rows:auto minmax(0,1fr) auto;height:100%;min-height:0;width:100%;min-width:0}.designer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;border:1px solid inherit;border-radius:10px;background:transparent;padding:10px 12px;margin-bottom:8px}.designer-current-dashboard{min-width:220px;font-size:.86rem;font-weight:700;color:inherit}.designer-toolbar-spacer{flex:1 1 auto;min-width:16px}.designer-toolbar-menu-group{display:inline-flex}.designer-toolbar-menu-group button{height:32px;border:1px solid #2f67d8;border-radius:6px;background:#2f67d8;color:#fff;padding:0 10px;font-size:.82rem;cursor:pointer}.designer-toolbar-menu-group button[disabled]{opacity:.6;cursor:not-allowed}.designer-main{min-height:0;min-width:0;width:100%;overflow:hidden;direction:ltr}.designer-main-splitter{height:100%;width:100%;min-width:0;direction:ltr}.designer-toolbox{font-size:3rem}.designer-toolbox ul{list-style-type:none;padding:0;margin:0;width:100%}.designer-toolbox li{float:left;margin-left:5px;margin-top:5px}.designer-toolbox .tool-groups{display:block}.designer-toolbox .tool-group-item{float:none;margin-left:0;margin-top:10px;clear:both}.designer-toolbox .tool-group-title{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.7;margin:0 0 4px 6px}.designer-toolbox .tool-group-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}.designer-toolbox .tool-group-list li{float:none;margin-left:0;margin-top:0}.designer-toolbox .props{font-size:1rem}.designer-toolbox .props li{float:none;padding-bottom:15px}.designer-toolbox .prop-section{width:100%;border:1px solid inherit;border-radius:6px;margin-top:8px;background:transparent;padding:6px 8px 2px}.designer-toolbox .prop-section-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;color:inherit;margin-bottom:6px}.palette-panel{height:100%;overflow:auto;width:100%}.designer-color-alpha-row{display:flex;align-items:center;gap:8px;margin-top:6px;margin-bottom:6px}.designer-color-alpha-label{font-size:.8rem;min-width:40px}.designer-color-alpha-row input[type=range]{flex:1 1 auto;min-width:90px}.designer-color-alpha-row input[type=number]{width:72px}.properties-panel{height:100%;overflow:hidden;width:100%;display:flex;flex-direction:column;min-height:0;padding-right:10px;padding-left:10px;padding-top:10px}.properties-panel>.designer-toolbox{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable}:host ::ng-deep .designer-left-bottom-splitter .p-splitterpanel{min-height:0!important;overflow:hidden!important}.hierarchy-panel{height:100%;width:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding:6px}.hierarchy-panel-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#4b5563;margin:0 0 6px}.hierarchy-tree-wrap{flex:1 1 auto;min-height:0;border:1px solid inherit;border-radius:6px;background:transparent;overflow:auto;padding:4px}:host ::ng-deep .hierarchy-tree-wrap .p-tree{border:none;padding:0;height:100%;background:transparent}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content{border:1px solid transparent;border-radius:4px;background:transparent!important;color:inherit!important;padding:0!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight{border-color:transparent!important;background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content:hover{background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label{display:inline-block;border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label:hover{border-color:#94a3b8}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label.active{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight .hierarchy-node-label{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}.designer-hierarchy-node{cursor:pointer;border:1px solid transparent;border-radius:4px;padding:2px 6px;font-size:.82rem;line-height:1.25rem}.designer-hierarchy-node-label{cursor:pointer;display:inline-block;width:100%}.designer-hierarchy-node.active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.designer-footer-item.designer-tree-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px}.dashboard-design{height:100%;min-height:0;min-width:0;width:100%;overflow:auto;direction:ltr}:host ::ng-deep .dashboard-design.designer-hide-datasources .datasource-descriptor{display:none!important}.current-dashboard-title{font-weight:600;margin-right:12px}.designer-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid inherit;background:transparent;min-height:36px;flex:0 0 auto;width:100%;overflow:auto}.designer-footer-label{font-size:.85rem;font-weight:600;color:inherit;white-space:nowrap}.designer-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.designer-footer-item{border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap}.designer-footer-item.active{border-color:#3b82f6;background:transparent;color:#1d4ed8}:host ::ng-deep .designer-footer-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px;box-shadow:inset 0 0 0 1px #3b82f6!important}.route-popup-form{display:flex;flex-direction:column;gap:8px}.route-popup-form label{font-size:.85rem;font-weight:600;color:#2f3f58}.route-popup-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.route-popup-btn{border:1px solid inherit;background:transparent;color:inherit;border-radius:6px;padding:7px 12px;font-size:.85rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f67d8;background:#2f67d8;color:#fff}.p-colorpicker{border:1px solid black;border-radius:4px}\n"] }]
13297
+ args: [{ selector: 'wuic-designer', imports: [NgClass, TranslateModule, AsyncPipe, FormsModule, SplitterModule, DragDropModule, SelectModule, ColorPickerModule, AutoCompleteModule, DashboardComponent, FieldEditorComponent, ContextMenuModule, MetadataEditorComponent, MenubarModule, ArchetypeConfiguratorComponent, ButtonModule, MultiSelectModule, CssSheetEditorComponent, MenuModule, DialogModule, TreeModule], providers: [TreeDragDropService], template: "<!-- <button pButton type=\"button\" (click)=\"loadDashboard()\" class=\"mr-2\">{{'load' | translate}}</button> -->\n@if (!noEdit) {\n<div class=\"designer-layout\">\n <div class=\"designer-toolbar\">\n <div class=\"designer-current-dashboard\">{{ currentDashboardTitle }}</div>\n\n <div class=\"designer-toolbar-spacer\"></div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"undo()\" [disabled]=\"!canUndo\">{{ 'designer.undo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"redo()\" [disabled]=\"!canRedo\">{{ 'designer.redo' | translate }}</button>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" data-testid=\"designer-actions-menu-btn\" (click)=\"refreshGraphActionsMenuItems(); graphActionsMenu.toggle($event)\">{{ 'designer.actions_menu' | translate }}</button>\n <p-menu #graphActionsMenu [popup]=\"true\" [model]=\"graphActionsMenuItems\" appendTo=\"body\"></p-menu>\n </div>\n\n <div class=\"designer-toolbar-menu-group\">\n <button type=\"button\" (click)=\"openCurrentDashboardInNewTab()\" [disabled]=\"!currentDashboardRoute\">{{\n 'designer.open_current_dashboard' | translate }}</button>\n </div>\n </div>\n <div class=\"designer-main\">\n <p-splitter [style]=\"{ height: '100%' }\" [panelSizes]=\"[15, 85]\" styleClass=\"designer-main-splitter\">\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[35, 65]\"\n [minSizes]=\"[20, 30]\" styleClass=\"designer-left-splitter\">\n <ng-template pTemplate>\n <div class=\"col flex designer-toolbox palette-panel\">\n <ul class=\"tool-groups\">\n @for (toolGroup of availableToolGroups; track toolGroup.group) {\n <li class=\"tool-group-item\">\n <div class=\"tool-group-title\">{{ toolGroup.group }}</div>\n <ul class=\"tool-group-list\">\n @for (tool of toolGroup.tools; track tool) {\n <li pDraggable (onDragStart)=\"dragStart(tool)\" (onDragEnd)=\"dragEnd()\" (onDrag)=\"onDrag($event)\"\n (dblclick)=\"dropToolAtRoot(tool)\">\n <i [ngClass]=\"tool.icon\" [title]=\"tool.name\"></i>\n </li>\n }\n </ul>\n </li>\n }\n </ul>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <p-splitter layout=\"vertical\" [style]=\"{ height: '100%', width: '100%' }\" [panelSizes]=\"[68, 32]\"\n [minSizes]=\"[35, 20]\" styleClass=\"designer-left-bottom-splitter\">\n <ng-template pTemplate>\n <div class=\"properties-panel\">\n <p-select [style]=\"{width: '100%'}\" [options]=\"flattenedDashboardElements\" [(ngModel)]=\"tool\"\n optionLabel=\"uniqueName\" dataKey=\"componentId\" appendTo=\"body\" />\n <div class=\"designer-toolbox\">\n\n <span style=\"width: 100%;\">\n <input type=\"text\" style=\"width: calc(100% - 30px);\" [(ngModel)]=\"propFilter\"\n [placeholder]=\"'prop.filter' | translate\" />\n <button style=\"width: 25px\" (click)=\"clearPropFilter()\">X</button>\n </span>\n\n @for (section of getToolPropertySections(tool, propFilter); track section.key) {\n <details class=\"prop-section\" [open]=\"section.open\"\n (toggle)=\"onPropertySectionToggle(tool, section.key, $event)\">\n <summary class=\"prop-section-title\">{{ section.label }}</summary>\n <ul class=\"props\">\n @for (toolProp of section.props; track toolProp.key) {\n <li>\n @if (!toolProp.value.hideCaption) {\n <span>{{ toolProp.value?.propertyCaption ||\n toolProp.key[0].toUpperCase() + toolProp.key.slice(1)\n }}</span>\n }\n <br />\n @if (toolProp.value.type == 'button') {\n <p-button (click)=\"toolProp.value.callback(tool.inputs, toolProp.key, null, tool)\"\n class=\"k-button\" [style]=\"{width: '100%'}\">\n {{toolProp.value.propertyCaption}}\n </p-button>\n }\n @if (toolProp.value.type == 'metaEditor' &&\n tool.inputs['componentRef']?.value?.component) {\n <wuic-metadata-editor [hardcodedDatasource]=\"tool.inputs['componentRef']?.value?.component\"\n [saveCallback]=\"metaEditorSave\"></wuic-metadata-editor>\n }\n @if (toolProp.value.type == 'boolean') {\n <input type=\"checkbox\" [(ngModel)]=\"tool.inputs[toolProp.key]\" />\n }\n @if (toolProp.value.type == 'numberToArray') {\n <input style=\"width: 100%;\" type=\"number\" pInputText [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'txt_area') {\n <textarea style=\"width: 100%;\" [(ngModel)]=\"tool.inputs[toolProp.key]\"\n [ngModelOptions]=\"{ updateOn: 'blur' }\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\"></textarea>\n }\n @if (toolProp.value.type == 'string') {\n <input style=\"width: 100%;\" type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'color') {\n <p-colorPicker [ngModel]=\"getDesignerColorPickerHex(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorPickerHexChange($event, tool, toolProp)\" appendTo=\"body\" />\n <div class=\"designer-color-alpha-row\">\n <span class=\"designer-color-alpha-label\">{{ 'designer.alpha' | translate }}</span>\n <input type=\"range\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n <input type=\"number\" min=\"0\" max=\"1\" step=\"0.01\"\n [ngModel]=\"getDesignerColorAlpha(tool, toolProp)\"\n (ngModelChange)=\"onDesignerColorAlphaChange($event, tool, toolProp)\" />\n </div>\n <input type=\"text\" pInputText [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (toolProp.value.type == 'dictionary' && !toolProp.value.async) {\n @if (toolProp.key == 'cssFile') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"availableProjectCssFiles\"\n [ngModel]=\"tool.inputs[toolProp.key]\" (ngModelChange)=\"onCssFileChanged($event, tool)\" />\n } @else if (toolProp.key == 'cssClass') {\n <p-multiSelect [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getCssClassOptionsForTool(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"onCssClassChanged($event, tool)\" [filter]=\"true\" [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"38\" scrollHeight=\"260px\" [showToggleAll]=\"false\" display=\"chip\"\n [maxSelectedLabels]=\"3\" [selectedItemsLabel]=\"'{0} selected'\">\n </p-multiSelect>\n } @else if ((tool.name == 'SELECT' || tool.name == 'MULTISELECT' || tool.name == 'UL') &&\n (toolProp.key == 'valueField' || toolProp.key == 'textField')) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else if (tool.name == 'LABEL' && toolProp.key == 'displayField') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [options]=\"getDatasourceColumnOptions(tool)\" [ngModel]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n } @else {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [(ngModel)]=\"tool.inputs[toolProp.key]\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n }\n @if (toolProp.value.type == 'dictionary' && toolProp.value.async) {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\" [options]=\"toolProp.value.values\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" />\n }\n @if (tool.name == 'DATAREPEATER' && toolProp.key == 'action' &&\n canOpenArchetypeConfigurator(tool)) {\n <button style=\"margin-top: 8px;\" (click)=\"openArchetypeConfigurator(tool)\">\n {{ 'designer.configure' | translate }} {{ (tool.inputs['action'] | async) || '' }}\n </button>\n }\n @if (toolProp.value.type == 'dropped-component-list') {\n <p-select [style]=\"{width: '100%'}\" appendTo=\"body\"\n [placeholder]=\"'select.component' | translate\" [options]=\"elementFilter(toolProp)\"\n [ngModel]=\"getDroppedComponentListModel(tool, toolProp)\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" optionLabel=\"uniqueName\"\n dataKey=\"uniqueName\" />\n }\n @if (toolProp.value.type == 'autocomplete') {\n <p-autoComplete [style]=\"{width: '100%'}\" appendTo=\"body\"\n [ngModel]=\"tool.inputs[toolProp.key] | async\" [forceSelection]=\"true\"\n (ngModelChange)=\"setValue($event, tool, toolProp)\" [suggestions]=\"tool.suggestions\"\n [optionLabel]=\"toolProp.value.displayField\" [optionValue]=\"toolProp.value.valueField\"\n (completeMethod)=\"search($event, tool, toolProp)\" [dropdown]=\"true\" dropdownMode=\"current\"\n [multiple]=\"toolProp.value.multiple\" />\n }\n @if (toolProp.value.type == 'propertyTree' && shouldShowPropertyTree(tool)) {\n <ul>\n @for (item of $any(tool.inputs[toolProp.key] | async); track item) {\n <li style=\"padding: 0; margin: 0;\">\n @if (item.col && tool.inputs['customProps_' + (tool.inputs['action'] | async)])\n {\n <wuic-field-editor [field]=\"item.col\"\n [record]=\"tool.inputs['customProps_' + (tool.inputs['action'] | async)]\"\n [metaInfo]=\"tool.inputs['datasource']?.component?.value?.metaInfo\"\n [triggerProp]=\"tool.inputs['action']\"></wuic-field-editor>\n }\n </li>\n }\n </ul>\n }\n </li>\n }\n </ul>\n </details>\n }\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"hierarchy-panel\">\n <div class=\"hierarchy-panel-title\">{{ 'designer.canvas_hierarchy' | translate }}</div>\n <div class=\"hierarchy-tree-wrap\">\n <p-tree [value]=\"hierarchyTreeNodes\" [draggableNodes]=\"true\" [droppableNodes]=\"true\"\n selectionMode=\"single\" [(selection)]=\"hierarchySelection\"\n (onNodeSelect)=\"onHierarchyNodeSelect($event)\" draggableScope=\"designer-hierarchy\"\n droppableScope=\"designer-hierarchy\" (onNodeDrop)=\"onHierarchyNodeDrop($event)\">\n <ng-template let-node pTemplate=\"default\">\n <span class=\"hierarchy-node-label\" [class.active]=\"isHierarchyNodeSelected(node)\"\n (mouseenter)=\"onHierarchyNodeMouseEnter(node)\" (mouseleave)=\"onHierarchyNodeMouseLeave()\"\n (contextmenu)=\"showHierarchyNodeContextMenu($event, node, hierarchyCtxMenu)\">\n {{ node.label }}\n </span>\n </ng-template>\n </p-tree>\n </div>\n <p-contextMenu #hierarchyCtxMenu [model]=\"hierarchyCtxItems\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </ng-template>\n </p-splitter>\n </ng-template>\n <ng-template pTemplate>\n <div class=\"col dashboard-design\" [class.designer-hide-datasources]=\"hideDatasourcesInCanvas\"\n [class.designer-snap-grid]=\"snapToGridEnabled\" [style.--wuic-grid-size.px]=\"gridSize\" pDroppable\n (onDrop)=\"drop($event)\" #canvas (mousedown)=\"onCanvasMouseDown($event)\"\n (contextmenu)=\"showCtx($event, this)\">\n <wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n @if (rubberBandVisible) {\n <div class=\"designer-selection-rect\" [style.left]=\"rubberBandStyle.left\" [style.top]=\"rubberBandStyle.top\"\n [style.width]=\"rubberBandStyle.width\" [style.height]=\"rubberBandStyle.height\"></div>\n }\n <p-contextMenu [target]=\"canvas\" [model]=\"ctxItems | async\" appendTo=\"body\" />\n </div>\n </ng-template>\n </p-splitter>\n </div>\n <div class=\"designer-footer\">\n <div class=\"designer-footer-label\">{{ 'designer.items' | translate }}:</div>\n <div class=\"designer-footer-list\">\n @for (item of droppedDashboardItems; track item.uniqueName) {\n <button type=\"button\" class=\"designer-footer-item\" [class.active]=\"tool?.uniqueName === item?.uniqueName\"\n [attr.data-designer-unique-name]=\"item?.uniqueName\" (mouseenter)=\"setFooterHoverHighlight(item)\"\n (mouseleave)=\"clearFooterHoverHighlight()\" (click)=\"selectDashboardItem(item)\"\n (contextmenu)=\"showFooterContextMenu($event, item, footerCtxMenu)\">\n {{ getDroppedDashboardItemLabel(item) }}\n </button>\n }\n </div>\n <p-contextMenu #footerCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n}\n\n@if (noEdit) {\n<wuic-dashboard [dashboardElements]=\"dashboardElements\"></wuic-dashboard>\n}\n\n<wuic-archetype-configurator [visible]=\"showArchetypeConfigurator\"\n (visibleChange)=\"onArchetypeConfigVisibleChange($event)\" [archetype]=\"getActiveArchetypeConfig()\"\n [metaInfo]=\"getArchetypeConfigMetaInfoForTool(archetypeConfigTool)\"\n [value]=\"getArchetypeConfigValueForTool(archetypeConfigTool)\" (applyConfig)=\"onArchetypeConfigApply($event)\">\n</wuic-archetype-configurator>\n\n<wuic-css-sheet-editor [visible]=\"showCssSheetEditor\" (visibleChange)=\"onCssSheetEditorVisibleChange($event)\"\n [sheets]=\"dashboardCssSheets\" [selectedSheetPath]=\"selectedCssEditorSheetPath\" [assetsFolder]=\"currentDashboardRoute\"\n [styleProps]=\"getCssEditorStyleProps()\" (applyEditor)=\"onCssSheetEditorApply($event)\">\n</wuic-css-sheet-editor>\n\n<p-dialog [header]=\"'designer.reopen_dashboard' | translate\" [(visible)]=\"reopenDashboardDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenDashboardDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"designer-reopen-select\">{{ 'designer.existing_dashboards' | translate }}</label>\n <p-select inputId=\"designer-reopen-select\" [options]=\"savedDashboardOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedDashboardRoute\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenDashboardDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenDashboardDialog()\"\n [disabled]=\"!reopenDialogSelectedDashboardRoute\">{{ 'designer.reopen' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;width:100%;height:100%;min-width:0}.designer-layout{display:grid;grid-template-rows:auto minmax(0,1fr) auto;height:100%;min-height:0;width:100%;min-width:0}.designer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;border:1px solid inherit;border-radius:10px;background:transparent;padding:10px 12px;margin-bottom:8px}.designer-current-dashboard{min-width:220px;font-size:.86rem;font-weight:700;color:inherit}.designer-toolbar-spacer{flex:1 1 auto;min-width:16px}.designer-toolbar-menu-group{display:inline-flex}.designer-toolbar-menu-group button{height:32px;border:1px solid #2f67d8;border-radius:6px;background:#2f67d8;color:#fff;padding:0 10px;font-size:.82rem;cursor:pointer}.designer-toolbar-menu-group button[disabled]{opacity:.6;cursor:not-allowed}.designer-main{min-height:0;min-width:0;width:100%;overflow:hidden;direction:ltr}.designer-main-splitter{height:100%;width:100%;min-width:0;direction:ltr}.designer-toolbox{font-size:3rem}.designer-toolbox ul{list-style-type:none;padding:0;margin:0;width:100%}.designer-toolbox li{float:left;margin-left:5px;margin-top:5px}.designer-toolbox .tool-groups{display:block}.designer-toolbox .tool-group-item{float:none;margin-left:0;margin-top:10px;clear:both}.designer-toolbox .tool-group-title{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.7;margin:0 0 4px 6px}.designer-toolbox .tool-group-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}.designer-toolbox .tool-group-list li{float:none;margin-left:0;margin-top:0}.designer-toolbox .props{font-size:1rem}.designer-toolbox .props li{float:none;padding-bottom:15px}.designer-toolbox .prop-section{width:100%;border:1px solid inherit;border-radius:6px;margin-top:8px;background:transparent;padding:6px 8px 2px}.designer-toolbox .prop-section-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;color:inherit;margin-bottom:6px}.palette-panel{height:100%;overflow:auto;width:100%}.designer-color-alpha-row{display:flex;align-items:center;gap:8px;margin-top:6px;margin-bottom:6px}.designer-color-alpha-label{font-size:.8rem;min-width:40px}.designer-color-alpha-row input[type=range]{flex:1 1 auto;min-width:90px}.designer-color-alpha-row input[type=number]{width:72px}.properties-panel{height:100%;overflow:hidden;width:100%;display:flex;flex-direction:column;min-height:0;padding-right:10px;padding-left:10px;padding-top:10px}.properties-panel>.designer-toolbox{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable}:host ::ng-deep .designer-left-bottom-splitter .p-splitterpanel{min-height:0!important;overflow:hidden!important}.hierarchy-panel{height:100%;width:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding:6px}.hierarchy-panel-title{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#4b5563;margin:0 0 6px}.hierarchy-tree-wrap{flex:1 1 auto;min-height:0;border:1px solid inherit;border-radius:6px;background:transparent;overflow:auto;padding:4px}:host ::ng-deep .hierarchy-tree-wrap .p-tree{border:none;padding:0;height:100%;background:transparent}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content{border:1px solid transparent;border-radius:4px;background:transparent!important;color:inherit!important;padding:0!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight{border-color:transparent!important;background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content:hover{background:transparent!important;color:inherit!important}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label{display:inline-block;border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label:hover{border-color:#94a3b8}:host ::ng-deep .hierarchy-tree-wrap .hierarchy-node-label.active{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}:host ::ng-deep .hierarchy-tree-wrap .p-tree-node-content.p-highlight .hierarchy-node-label{border-color:#3b82f6!important;background:#eff6ff!important;color:#1d4ed8!important}.designer-hierarchy-node{cursor:pointer;border:1px solid transparent;border-radius:4px;padding:2px 6px;font-size:.82rem;line-height:1.25rem}.designer-hierarchy-node-label{cursor:pointer;display:inline-block;width:100%}.designer-hierarchy-node.active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.designer-footer-item.designer-tree-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px}.dashboard-design{height:100%;min-height:0;min-width:0;width:100%;overflow:auto;direction:ltr;position:relative}.designer-selection-rect{position:absolute;border:1px dashed #3b82f6;background:#3b82f61f;pointer-events:none;z-index:10000}:host ::ng-deep .designer-multi-selected{outline:2px solid #3b82f6!important;outline-offset:1px}:host ::ng-deep .dashboard-design.designer-hide-datasources .datasource-descriptor{display:none!important}.current-dashboard-title{font-weight:600;margin-right:12px}.designer-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid inherit;background:transparent;min-height:36px;flex:0 0 auto;width:100%;overflow:auto}.designer-footer-label{font-size:.85rem;font-weight:600;color:inherit;white-space:nowrap}.designer-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.designer-footer-item{border:1px solid inherit;background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap}.designer-footer-item.active{border-color:#3b82f6;background:transparent;color:#1d4ed8}:host ::ng-deep .designer-footer-hover-target{border-color:#3b82f6!important;outline:1px solid #3b82f6!important;outline-offset:-1px;box-shadow:inset 0 0 0 1px #3b82f6!important}.route-popup-form{display:flex;flex-direction:column;gap:8px}.route-popup-form label{font-size:.85rem;font-weight:600;color:#2f3f58}.route-popup-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.route-popup-btn{border:1px solid inherit;background:transparent;color:inherit;border-radius:6px;padding:7px 12px;font-size:.85rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f67d8;background:#2f67d8;color:#fff}.p-colorpicker{border:1px solid black;border-radius:4px}.dashboard-design.designer-snap-grid{background-image:linear-gradient(to right,rgba(128,128,128,.16) 1px,transparent 1px),linear-gradient(to bottom,rgba(128,128,128,.16) 1px,transparent 1px);background-size:var(--wuic-grid-size, 10px) var(--wuic-grid-size, 10px)}\n"] }]
11836
13298
  }], ctorParameters: () => [{ type: i1$1.ActivatedRoute }, { type: DataProviderService }, { type: MetadataProviderService }, { type: UserInfoService }, { type: i0.ChangeDetectorRef }, { type: TranslationManagerService }, { type: MetadataEditorService }, { type: ChatbotHostRegistryService }, { type: WorkflowRuntimeMetadataService }], propDecorators: { onWindowHashChange: [{
11837
13299
  type: HostListener,
11838
13300
  args: ['window:hashchange']
@@ -11854,7 +13316,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
11854
13316
  }], onWindowChange: [{
11855
13317
  type: HostListener,
11856
13318
  args: ['window:change']
13319
+ }], canvasRef: [{
13320
+ type: ViewChild,
13321
+ args: ['canvas']
11857
13322
  }] } });
11858
13323
 
11859
13324
  export { DesignerComponent };
11860
- //# sourceMappingURL=wuic-framework-lib-designer.component-N_CfbGnr.mjs.map
13325
+ //# sourceMappingURL=wuic-framework-lib-designer.component-x10ZzUEy.mjs.map