wuic-framework-lib 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-Dq9IEKsY.mjs → wuic-framework-lib-chart-list.component-MeAS47VV.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-code-editor.component-B6IpTSo8.mjs → wuic-framework-lib-code-editor.component-WtZC2MZC.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-designer.component-D6cbqNFg.mjs → wuic-framework-lib-designer.component-Dt7Yz5hn.mjs} +12 -12
- package/fesm2022/{wuic-framework-lib-first-run-wizard.component-D1ZQUl-q.mjs → wuic-framework-lib-first-run-wizard.component-C8nhy5yK.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-framework-docs.component-C_o5r7pg.mjs → wuic-framework-lib-framework-docs.component-CGsugPCx.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-DkWGzJpr.mjs → wuic-framework-lib-pivot-builder.component-S5zqy3vj.mjs} +164 -164
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-D29go5yA.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-VY-gtgRW.mjs} +28 -28
- package/fesm2022/{wuic-framework-lib-report-designer.component-DcpCWghx.mjs → wuic-framework-lib-report-designer.component-QRxxueU-.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-report-scaffold-dialog.component-CgUjG7Bi.mjs → wuic-framework-lib-report-scaffold-dialog.component-OGdglGPw.mjs} +246 -246
- package/fesm2022/{wuic-framework-lib-report-viewer.component-C4Opgl_M.mjs → wuic-framework-lib-report-viewer.component-CuXw3e0j.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-scene3d-designer.component-8l3vsyEb.mjs → wuic-framework-lib-scene3d-designer.component-LhC0KzWO.mjs} +20 -20
- package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-C1w-8Aai.mjs → wuic-framework-lib-scene3d-light-baker-gpu-B5ywsrSp.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs → wuic-framework-lib-scene3d-shader-effect-gBEFKNvi.mjs} +18 -18
- package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-nMSl5jbe.mjs → wuic-framework-lib-scene3d-viewer.component-DiDqJO-P.mjs} +5 -5
- package/fesm2022/{wuic-framework-lib-scheduler-list.component-Cvk3Zg0Q.mjs → wuic-framework-lib-scheduler-list.component-5JbxIBdp.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-C1U7JPiq.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Dxk5d9au.mjs} +47 -47
- package/fesm2022/{wuic-framework-lib-timeline-list.component-cZbPgNh1.mjs → wuic-framework-lib-timeline-list.component-B1ro-16J.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-workflow-designer.component-C_4RQGa0.mjs → wuic-framework-lib-workflow-designer.component-D32ZMxq2.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-BZZwta8b.mjs → wuic-framework-lib-wuic-framework-lib-81WUtxz8.mjs} +821 -821
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +1 -1
- package/styles/_wuic-tokens.scss +82 -82
|
@@ -25,7 +25,7 @@ import { TableModule } from 'primeng/table';
|
|
|
25
25
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'primeng/tabs';
|
|
26
26
|
import * as i1$3 from '@materia-ui/ngx-monaco-editor';
|
|
27
27
|
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
|
|
28
|
-
import { W as WtoolboxService, U as UserInfoService, T as TranslationManagerService, M as MetadataProviderService, h as DataProviderService, o as MetaInfo, g as MetadatiColonna, q as FilterInfo, G as GlobalHandler, m as WuicClientException, n as WuicErrorCodes, r as LicenseFeatureService, C as ChatbotHostRegistryService, b as DataSourceComponent, F as FilterBarComponent } from './wuic-framework-lib-wuic-framework-lib-
|
|
28
|
+
import { W as WtoolboxService, U as UserInfoService, T as TranslationManagerService, M as MetadataProviderService, h as DataProviderService, o as MetaInfo, g as MetadatiColonna, q as FilterInfo, G as GlobalHandler, m as WuicClientException, n as WuicErrorCodes, r as LicenseFeatureService, C as ChatbotHostRegistryService, b as DataSourceComponent, F as FilterBarComponent } from './wuic-framework-lib-wuic-framework-lib-81WUtxz8.mjs';
|
|
29
29
|
import * as i7 from 'primeng/tooltip';
|
|
30
30
|
import { TooltipModule } from 'primeng/tooltip';
|
|
31
31
|
import { ClassicPreset, NodeEditor } from 'rete';
|
|
@@ -160,168 +160,168 @@ class ViewBuilderNodeComponent {
|
|
|
160
160
|
this.cdr.detectChanges();
|
|
161
161
|
}
|
|
162
162
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderNodeComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
163
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: ViewBuilderNodeComponent, isStandalone: true, selector: "wuic-view-builder-node", inputs: { data: "data", emit: "emit", rendered: "rendered" }, host: { attributes: { "data-testid": "vb-node" }, properties: { "style.width.px": "this.width", "style.height.px": "this.height" } }, usesOnChanges: true, ngImport: i0, template: `
|
|
164
|
-
<div class="vb-node" [class.vb-node--selected]="selected">
|
|
165
|
-
<div class="vb-node__header" [title]="nodeData?.route || ''"
|
|
166
|
-
(pointerdown)="onHeaderPointerDown($event)"
|
|
167
|
-
(pointerup)="onHeaderPointerUp($event)"
|
|
168
|
-
(contextmenu)="onHeaderContextMenu($event)"
|
|
169
|
-
style="cursor:pointer;">
|
|
170
|
-
<i class="pi" [ngClass]="collapsed ? 'pi-chevron-right' : 'pi-chevron-down'" style="font-size:0.7rem;"></i>
|
|
171
|
-
<i class="pi pi-table"></i>
|
|
172
|
-
<!-- Toggle-all checkbox: seleziona/deseleziona tutte le colonne non-virtuali. stopPropagation su pointerdown/pointerup/click isola dal listener dell'header (collapse toggle) senza rompere il comportamento default del checkbox. -->
|
|
173
|
-
<input type="checkbox" class="vb-node__toggle-all"
|
|
174
|
-
[checked]="allSelectableSelected"
|
|
175
|
-
title="Seleziona/deseleziona tutti i campi"
|
|
176
|
-
(pointerdown)="$event.stopPropagation()"
|
|
177
|
-
(pointerup)="$event.stopPropagation()"
|
|
178
|
-
(click)="onToggleAllClick($event)" />
|
|
179
|
-
<span class="vb-node__title">{{ nodeData?.caption || nodeData?.route || 'Table' }}</span>
|
|
180
|
-
<span class="vb-node__alias">({{ nodeData?.tableAlias }})</span>
|
|
181
|
-
<span class="vb-node__col-count">({{ selectedCount }})</span>
|
|
182
|
-
<i class="pi pi-times vb-node__remove-btn" title="Remove table"
|
|
183
|
-
(pointerdown)="$event.stopPropagation()"
|
|
184
|
-
(click)="removeNode($event)"></i>
|
|
185
|
-
</div>
|
|
186
|
-
<!-- Collapsed: show sockets on the header row -->
|
|
187
|
-
<div *ngIf="collapsed" class="vb-node__collapsed-sockets">
|
|
188
|
-
<ng-container *ngFor="let col of nodeData?.columns || []">
|
|
189
|
-
<div class="vb-node__socket-out vb-node__socket-collapsed"
|
|
190
|
-
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
191
|
-
*ngIf="$any(data).outputs[col.alias]"
|
|
192
|
-
refComponent
|
|
193
|
-
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
194
|
-
[emit]="emit"></div>
|
|
195
|
-
<div class="vb-node__socket-in vb-node__socket-collapsed"
|
|
196
|
-
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
197
|
-
*ngIf="$any(data).inputs[col.alias]"
|
|
198
|
-
refComponent
|
|
199
|
-
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
200
|
-
[emit]="emit"></div>
|
|
201
|
-
</ng-container>
|
|
202
|
-
</div>
|
|
203
|
-
<!-- wheel stop: the column list is scrollable, rete-area on canvas must not eat the wheel gesture.
|
|
204
|
-
pointerdown stop ONLY when target is the scrollable container itself (scrollbar track click).
|
|
205
|
-
Child clicks (socket for connection drag, checkbox, label) must bubble freely so rete-connection-plugin
|
|
206
|
-
can start the drag. Without the target === currentTarget guard, stopping every pointerdown breaks
|
|
207
|
-
the connect-by-drag flow between two nodes (observed 2026-04-24 on self-join same-route). -->
|
|
208
|
-
<div class="vb-node__columns" *ngIf="!collapsed"
|
|
209
|
-
(wheel)="$event.stopPropagation()"
|
|
210
|
-
(pointerdown)="onColumnsPointerDown($event)">
|
|
211
|
-
<div *ngFor="let col of nodeData?.columns || []" class="vb-node__col-row"
|
|
212
|
-
[class.vb-node__col-row--virtual]="col.virtual"
|
|
213
|
-
[class.vb-node__col-row--has-formula]="col.formula"
|
|
214
|
-
(contextmenu)="onColumnContextMenu($event, col)">
|
|
215
|
-
<label class="vb-node__col-label" (pointerdown)="$event.stopPropagation()"
|
|
216
|
-
[pTooltip]="col.formula ? 'Formula: ' + col.formula + (col.formulaAlias ? ' AS ' + col.formulaAlias : '') : col.alias + ' | ui: ' + (col.uiType || '-') + ' | db: ' + (col.dbType || '-')"
|
|
217
|
-
tooltipPosition="left">
|
|
218
|
-
<input type="checkbox" [(ngModel)]="col.selected"
|
|
219
|
-
[disabled]="col.virtual"
|
|
220
|
-
(ngModelChange)="toggleColumn(col, $event)" />
|
|
221
|
-
<i *ngIf="col.formula || col.formulaAlias" class="pi pi-bolt vb-node__formula-icon" title="Custom formula"></i>
|
|
222
|
-
<span [class.vb-node__col-fk]="col.uiType === 'lookupByID'"
|
|
223
|
-
[class.vb-node__col-formula]="col.formula || col.formulaAlias">{{ col.formulaAlias || col.label || col.alias }}<ng-container *ngIf="col.uiType === 'lookupByID' && col.alias !== col.label"> ({{ col.alias }})</ng-container></span>
|
|
224
|
-
<i *ngIf="col.uiType === 'lookupByID'" class="pi pi-link vb-node__fk-icon"
|
|
225
|
-
[title]="'FK → ' + col.lookupEntityName"></i>
|
|
226
|
-
</label>
|
|
227
|
-
<div class="vb-node__socket-out"
|
|
228
|
-
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
229
|
-
*ngIf="$any(data).outputs[col.alias]"
|
|
230
|
-
refComponent
|
|
231
|
-
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
232
|
-
[emit]="emit"></div>
|
|
233
|
-
<div class="vb-node__socket-in"
|
|
234
|
-
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
235
|
-
*ngIf="$any(data).inputs[col.alias]"
|
|
236
|
-
refComponent
|
|
237
|
-
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
238
|
-
[emit]="emit"></div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
163
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: ViewBuilderNodeComponent, isStandalone: true, selector: "wuic-view-builder-node", inputs: { data: "data", emit: "emit", rendered: "rendered" }, host: { attributes: { "data-testid": "vb-node" }, properties: { "style.width.px": "this.width", "style.height.px": "this.height" } }, usesOnChanges: true, ngImport: i0, template: `
|
|
164
|
+
<div class="vb-node" [class.vb-node--selected]="selected">
|
|
165
|
+
<div class="vb-node__header" [title]="nodeData?.route || ''"
|
|
166
|
+
(pointerdown)="onHeaderPointerDown($event)"
|
|
167
|
+
(pointerup)="onHeaderPointerUp($event)"
|
|
168
|
+
(contextmenu)="onHeaderContextMenu($event)"
|
|
169
|
+
style="cursor:pointer;">
|
|
170
|
+
<i class="pi" [ngClass]="collapsed ? 'pi-chevron-right' : 'pi-chevron-down'" style="font-size:0.7rem;"></i>
|
|
171
|
+
<i class="pi pi-table"></i>
|
|
172
|
+
<!-- Toggle-all checkbox: seleziona/deseleziona tutte le colonne non-virtuali. stopPropagation su pointerdown/pointerup/click isola dal listener dell'header (collapse toggle) senza rompere il comportamento default del checkbox. -->
|
|
173
|
+
<input type="checkbox" class="vb-node__toggle-all"
|
|
174
|
+
[checked]="allSelectableSelected"
|
|
175
|
+
title="Seleziona/deseleziona tutti i campi"
|
|
176
|
+
(pointerdown)="$event.stopPropagation()"
|
|
177
|
+
(pointerup)="$event.stopPropagation()"
|
|
178
|
+
(click)="onToggleAllClick($event)" />
|
|
179
|
+
<span class="vb-node__title">{{ nodeData?.caption || nodeData?.route || 'Table' }}</span>
|
|
180
|
+
<span class="vb-node__alias">({{ nodeData?.tableAlias }})</span>
|
|
181
|
+
<span class="vb-node__col-count">({{ selectedCount }})</span>
|
|
182
|
+
<i class="pi pi-times vb-node__remove-btn" title="Remove table"
|
|
183
|
+
(pointerdown)="$event.stopPropagation()"
|
|
184
|
+
(click)="removeNode($event)"></i>
|
|
185
|
+
</div>
|
|
186
|
+
<!-- Collapsed: show sockets on the header row -->
|
|
187
|
+
<div *ngIf="collapsed" class="vb-node__collapsed-sockets">
|
|
188
|
+
<ng-container *ngFor="let col of nodeData?.columns || []">
|
|
189
|
+
<div class="vb-node__socket-out vb-node__socket-collapsed"
|
|
190
|
+
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
191
|
+
*ngIf="$any(data).outputs[col.alias]"
|
|
192
|
+
refComponent
|
|
193
|
+
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
194
|
+
[emit]="emit"></div>
|
|
195
|
+
<div class="vb-node__socket-in vb-node__socket-collapsed"
|
|
196
|
+
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
197
|
+
*ngIf="$any(data).inputs[col.alias]"
|
|
198
|
+
refComponent
|
|
199
|
+
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
200
|
+
[emit]="emit"></div>
|
|
201
|
+
</ng-container>
|
|
202
|
+
</div>
|
|
203
|
+
<!-- wheel stop: the column list is scrollable, rete-area on canvas must not eat the wheel gesture.
|
|
204
|
+
pointerdown stop ONLY when target is the scrollable container itself (scrollbar track click).
|
|
205
|
+
Child clicks (socket for connection drag, checkbox, label) must bubble freely so rete-connection-plugin
|
|
206
|
+
can start the drag. Without the target === currentTarget guard, stopping every pointerdown breaks
|
|
207
|
+
the connect-by-drag flow between two nodes (observed 2026-04-24 on self-join same-route). -->
|
|
208
|
+
<div class="vb-node__columns" *ngIf="!collapsed"
|
|
209
|
+
(wheel)="$event.stopPropagation()"
|
|
210
|
+
(pointerdown)="onColumnsPointerDown($event)">
|
|
211
|
+
<div *ngFor="let col of nodeData?.columns || []" class="vb-node__col-row"
|
|
212
|
+
[class.vb-node__col-row--virtual]="col.virtual"
|
|
213
|
+
[class.vb-node__col-row--has-formula]="col.formula"
|
|
214
|
+
(contextmenu)="onColumnContextMenu($event, col)">
|
|
215
|
+
<label class="vb-node__col-label" (pointerdown)="$event.stopPropagation()"
|
|
216
|
+
[pTooltip]="col.formula ? 'Formula: ' + col.formula + (col.formulaAlias ? ' AS ' + col.formulaAlias : '') : col.alias + ' | ui: ' + (col.uiType || '-') + ' | db: ' + (col.dbType || '-')"
|
|
217
|
+
tooltipPosition="left">
|
|
218
|
+
<input type="checkbox" [(ngModel)]="col.selected"
|
|
219
|
+
[disabled]="col.virtual"
|
|
220
|
+
(ngModelChange)="toggleColumn(col, $event)" />
|
|
221
|
+
<i *ngIf="col.formula || col.formulaAlias" class="pi pi-bolt vb-node__formula-icon" title="Custom formula"></i>
|
|
222
|
+
<span [class.vb-node__col-fk]="col.uiType === 'lookupByID'"
|
|
223
|
+
[class.vb-node__col-formula]="col.formula || col.formulaAlias">{{ col.formulaAlias || col.label || col.alias }}<ng-container *ngIf="col.uiType === 'lookupByID' && col.alias !== col.label"> ({{ col.alias }})</ng-container></span>
|
|
224
|
+
<i *ngIf="col.uiType === 'lookupByID'" class="pi pi-link vb-node__fk-icon"
|
|
225
|
+
[title]="'FK → ' + col.lookupEntityName"></i>
|
|
226
|
+
</label>
|
|
227
|
+
<div class="vb-node__socket-out"
|
|
228
|
+
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
229
|
+
*ngIf="$any(data).outputs[col.alias]"
|
|
230
|
+
refComponent
|
|
231
|
+
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
232
|
+
[emit]="emit"></div>
|
|
233
|
+
<div class="vb-node__socket-in"
|
|
234
|
+
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
235
|
+
*ngIf="$any(data).inputs[col.alias]"
|
|
236
|
+
refComponent
|
|
237
|
+
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
238
|
+
[emit]="emit"></div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
242
|
`, isInline: true, styles: [":host{display:block}.vb-node{background:var(--surface-card, #fff);border:2px solid var(--surface-border, #dee2e6);border-radius:8px;min-width:240px;max-width:320px;font-size:.85rem;box-shadow:0 2px 8px #00000014;overflow:hidden}.vb-node--selected{border-color:var(--primary-color, #3B82F6)}.vb-node__header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--primary-color, #3B82F6);color:#fff;font-weight:600}.vb-node__alias{opacity:.7;font-size:.75rem}.vb-node__col-count{margin-left:auto;opacity:.8;font-size:.75rem}.vb-node__remove-btn{font-size:.7rem;opacity:.6;padding:2px 4px;cursor:pointer}.vb-node__remove-btn:hover{opacity:1}.vb-node__toggle-all{margin:0 4px 0 2px;cursor:pointer;vertical-align:middle}.vb-node__columns{max-height:260px;overflow-y:auto;padding:4px 0}.vb-node__col-row{display:flex;align-items:center;padding:2px 10px;position:relative;-webkit-user-select:none;user-select:none}.vb-node__col-row:hover{background:var(--surface-hover, #f4f4f4)}.vb-node__col-row--virtual{opacity:.45}.vb-node__col-row--has-formula{background:#eff6ff;border-left:2px solid var(--primary-color, #3B82F6)}.vb-node__col-row--virtual .vb-node__col-label{cursor:default}.vb-node__col-label{display:flex;align-items:center;gap:4px;flex:1;cursor:pointer;font-size:.8rem}.vb-node__col-fk{font-style:italic}.vb-node__col-formula{color:var(--primary-color, #3B82F6);font-weight:600}.vb-node__formula-icon{font-size:.65rem;color:#f59e0b;margin-right:1px}.vb-node__fk-icon{font-size:.7rem;color:var(--primary-color, #3B82F6)}.vb-node__socket-out,.vb-node__socket-in{width:16px;height:16px;border-radius:50%;border:2px solid var(--primary-color, #3B82F6);background:#fff;position:absolute;top:50%;transform:translateY(-50%);cursor:crosshair;z-index:10}.vb-node__socket-out{right:-8px}.vb-node__socket-out:hover{background:var(--primary-color, #3B82F6);border-color:#1d4ed8}.vb-node__socket-in{left:-8px}.vb-node__socket-in:hover{background:#4ade80;border-color:#16a34a}.vb-node__socket-out ::ng-deep>*,.vb-node__socket-in ::ng-deep>*{opacity:0;width:16px!important;height:16px!important;margin:0!important;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);cursor:crosshair}.vb-node__collapsed-sockets{position:relative;height:0;overflow:visible}.vb-node__socket-collapsed{position:absolute!important;top:-16px!important}.vb-node__socket-collapsed.vb-node__socket-out{right:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed.vb-node__socket-in{left:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed ::ng-deep>*{pointer-events:none!important}.vb-node__socket--connected ::ng-deep>*{opacity:1;background:#4ade80!important;border:2px solid #16a34a!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RefDirective, selector: "[refComponent]", inputs: ["data", "emit"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i7.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }] });
|
|
243
243
|
}
|
|
244
244
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderNodeComponent, decorators: [{
|
|
245
245
|
type: Component,
|
|
246
|
-
args: [{ selector: 'wuic-view-builder-node', standalone: true, imports: [CommonModule, FormsModule, RefDirective, TooltipModule], template: `
|
|
247
|
-
<div class="vb-node" [class.vb-node--selected]="selected">
|
|
248
|
-
<div class="vb-node__header" [title]="nodeData?.route || ''"
|
|
249
|
-
(pointerdown)="onHeaderPointerDown($event)"
|
|
250
|
-
(pointerup)="onHeaderPointerUp($event)"
|
|
251
|
-
(contextmenu)="onHeaderContextMenu($event)"
|
|
252
|
-
style="cursor:pointer;">
|
|
253
|
-
<i class="pi" [ngClass]="collapsed ? 'pi-chevron-right' : 'pi-chevron-down'" style="font-size:0.7rem;"></i>
|
|
254
|
-
<i class="pi pi-table"></i>
|
|
255
|
-
<!-- Toggle-all checkbox: seleziona/deseleziona tutte le colonne non-virtuali. stopPropagation su pointerdown/pointerup/click isola dal listener dell'header (collapse toggle) senza rompere il comportamento default del checkbox. -->
|
|
256
|
-
<input type="checkbox" class="vb-node__toggle-all"
|
|
257
|
-
[checked]="allSelectableSelected"
|
|
258
|
-
title="Seleziona/deseleziona tutti i campi"
|
|
259
|
-
(pointerdown)="$event.stopPropagation()"
|
|
260
|
-
(pointerup)="$event.stopPropagation()"
|
|
261
|
-
(click)="onToggleAllClick($event)" />
|
|
262
|
-
<span class="vb-node__title">{{ nodeData?.caption || nodeData?.route || 'Table' }}</span>
|
|
263
|
-
<span class="vb-node__alias">({{ nodeData?.tableAlias }})</span>
|
|
264
|
-
<span class="vb-node__col-count">({{ selectedCount }})</span>
|
|
265
|
-
<i class="pi pi-times vb-node__remove-btn" title="Remove table"
|
|
266
|
-
(pointerdown)="$event.stopPropagation()"
|
|
267
|
-
(click)="removeNode($event)"></i>
|
|
268
|
-
</div>
|
|
269
|
-
<!-- Collapsed: show sockets on the header row -->
|
|
270
|
-
<div *ngIf="collapsed" class="vb-node__collapsed-sockets">
|
|
271
|
-
<ng-container *ngFor="let col of nodeData?.columns || []">
|
|
272
|
-
<div class="vb-node__socket-out vb-node__socket-collapsed"
|
|
273
|
-
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
274
|
-
*ngIf="$any(data).outputs[col.alias]"
|
|
275
|
-
refComponent
|
|
276
|
-
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
277
|
-
[emit]="emit"></div>
|
|
278
|
-
<div class="vb-node__socket-in vb-node__socket-collapsed"
|
|
279
|
-
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
280
|
-
*ngIf="$any(data).inputs[col.alias]"
|
|
281
|
-
refComponent
|
|
282
|
-
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
283
|
-
[emit]="emit"></div>
|
|
284
|
-
</ng-container>
|
|
285
|
-
</div>
|
|
286
|
-
<!-- wheel stop: the column list is scrollable, rete-area on canvas must not eat the wheel gesture.
|
|
287
|
-
pointerdown stop ONLY when target is the scrollable container itself (scrollbar track click).
|
|
288
|
-
Child clicks (socket for connection drag, checkbox, label) must bubble freely so rete-connection-plugin
|
|
289
|
-
can start the drag. Without the target === currentTarget guard, stopping every pointerdown breaks
|
|
290
|
-
the connect-by-drag flow between two nodes (observed 2026-04-24 on self-join same-route). -->
|
|
291
|
-
<div class="vb-node__columns" *ngIf="!collapsed"
|
|
292
|
-
(wheel)="$event.stopPropagation()"
|
|
293
|
-
(pointerdown)="onColumnsPointerDown($event)">
|
|
294
|
-
<div *ngFor="let col of nodeData?.columns || []" class="vb-node__col-row"
|
|
295
|
-
[class.vb-node__col-row--virtual]="col.virtual"
|
|
296
|
-
[class.vb-node__col-row--has-formula]="col.formula"
|
|
297
|
-
(contextmenu)="onColumnContextMenu($event, col)">
|
|
298
|
-
<label class="vb-node__col-label" (pointerdown)="$event.stopPropagation()"
|
|
299
|
-
[pTooltip]="col.formula ? 'Formula: ' + col.formula + (col.formulaAlias ? ' AS ' + col.formulaAlias : '') : col.alias + ' | ui: ' + (col.uiType || '-') + ' | db: ' + (col.dbType || '-')"
|
|
300
|
-
tooltipPosition="left">
|
|
301
|
-
<input type="checkbox" [(ngModel)]="col.selected"
|
|
302
|
-
[disabled]="col.virtual"
|
|
303
|
-
(ngModelChange)="toggleColumn(col, $event)" />
|
|
304
|
-
<i *ngIf="col.formula || col.formulaAlias" class="pi pi-bolt vb-node__formula-icon" title="Custom formula"></i>
|
|
305
|
-
<span [class.vb-node__col-fk]="col.uiType === 'lookupByID'"
|
|
306
|
-
[class.vb-node__col-formula]="col.formula || col.formulaAlias">{{ col.formulaAlias || col.label || col.alias }}<ng-container *ngIf="col.uiType === 'lookupByID' && col.alias !== col.label"> ({{ col.alias }})</ng-container></span>
|
|
307
|
-
<i *ngIf="col.uiType === 'lookupByID'" class="pi pi-link vb-node__fk-icon"
|
|
308
|
-
[title]="'FK → ' + col.lookupEntityName"></i>
|
|
309
|
-
</label>
|
|
310
|
-
<div class="vb-node__socket-out"
|
|
311
|
-
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
312
|
-
*ngIf="$any(data).outputs[col.alias]"
|
|
313
|
-
refComponent
|
|
314
|
-
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
315
|
-
[emit]="emit"></div>
|
|
316
|
-
<div class="vb-node__socket-in"
|
|
317
|
-
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
318
|
-
*ngIf="$any(data).inputs[col.alias]"
|
|
319
|
-
refComponent
|
|
320
|
-
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
321
|
-
[emit]="emit"></div>
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
246
|
+
args: [{ selector: 'wuic-view-builder-node', standalone: true, imports: [CommonModule, FormsModule, RefDirective, TooltipModule], template: `
|
|
247
|
+
<div class="vb-node" [class.vb-node--selected]="selected">
|
|
248
|
+
<div class="vb-node__header" [title]="nodeData?.route || ''"
|
|
249
|
+
(pointerdown)="onHeaderPointerDown($event)"
|
|
250
|
+
(pointerup)="onHeaderPointerUp($event)"
|
|
251
|
+
(contextmenu)="onHeaderContextMenu($event)"
|
|
252
|
+
style="cursor:pointer;">
|
|
253
|
+
<i class="pi" [ngClass]="collapsed ? 'pi-chevron-right' : 'pi-chevron-down'" style="font-size:0.7rem;"></i>
|
|
254
|
+
<i class="pi pi-table"></i>
|
|
255
|
+
<!-- Toggle-all checkbox: seleziona/deseleziona tutte le colonne non-virtuali. stopPropagation su pointerdown/pointerup/click isola dal listener dell'header (collapse toggle) senza rompere il comportamento default del checkbox. -->
|
|
256
|
+
<input type="checkbox" class="vb-node__toggle-all"
|
|
257
|
+
[checked]="allSelectableSelected"
|
|
258
|
+
title="Seleziona/deseleziona tutti i campi"
|
|
259
|
+
(pointerdown)="$event.stopPropagation()"
|
|
260
|
+
(pointerup)="$event.stopPropagation()"
|
|
261
|
+
(click)="onToggleAllClick($event)" />
|
|
262
|
+
<span class="vb-node__title">{{ nodeData?.caption || nodeData?.route || 'Table' }}</span>
|
|
263
|
+
<span class="vb-node__alias">({{ nodeData?.tableAlias }})</span>
|
|
264
|
+
<span class="vb-node__col-count">({{ selectedCount }})</span>
|
|
265
|
+
<i class="pi pi-times vb-node__remove-btn" title="Remove table"
|
|
266
|
+
(pointerdown)="$event.stopPropagation()"
|
|
267
|
+
(click)="removeNode($event)"></i>
|
|
268
|
+
</div>
|
|
269
|
+
<!-- Collapsed: show sockets on the header row -->
|
|
270
|
+
<div *ngIf="collapsed" class="vb-node__collapsed-sockets">
|
|
271
|
+
<ng-container *ngFor="let col of nodeData?.columns || []">
|
|
272
|
+
<div class="vb-node__socket-out vb-node__socket-collapsed"
|
|
273
|
+
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
274
|
+
*ngIf="$any(data).outputs[col.alias]"
|
|
275
|
+
refComponent
|
|
276
|
+
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
277
|
+
[emit]="emit"></div>
|
|
278
|
+
<div class="vb-node__socket-in vb-node__socket-collapsed"
|
|
279
|
+
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
280
|
+
*ngIf="$any(data).inputs[col.alias]"
|
|
281
|
+
refComponent
|
|
282
|
+
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
283
|
+
[emit]="emit"></div>
|
|
284
|
+
</ng-container>
|
|
285
|
+
</div>
|
|
286
|
+
<!-- wheel stop: the column list is scrollable, rete-area on canvas must not eat the wheel gesture.
|
|
287
|
+
pointerdown stop ONLY when target is the scrollable container itself (scrollbar track click).
|
|
288
|
+
Child clicks (socket for connection drag, checkbox, label) must bubble freely so rete-connection-plugin
|
|
289
|
+
can start the drag. Without the target === currentTarget guard, stopping every pointerdown breaks
|
|
290
|
+
the connect-by-drag flow between two nodes (observed 2026-04-24 on self-join same-route). -->
|
|
291
|
+
<div class="vb-node__columns" *ngIf="!collapsed"
|
|
292
|
+
(wheel)="$event.stopPropagation()"
|
|
293
|
+
(pointerdown)="onColumnsPointerDown($event)">
|
|
294
|
+
<div *ngFor="let col of nodeData?.columns || []" class="vb-node__col-row"
|
|
295
|
+
[class.vb-node__col-row--virtual]="col.virtual"
|
|
296
|
+
[class.vb-node__col-row--has-formula]="col.formula"
|
|
297
|
+
(contextmenu)="onColumnContextMenu($event, col)">
|
|
298
|
+
<label class="vb-node__col-label" (pointerdown)="$event.stopPropagation()"
|
|
299
|
+
[pTooltip]="col.formula ? 'Formula: ' + col.formula + (col.formulaAlias ? ' AS ' + col.formulaAlias : '') : col.alias + ' | ui: ' + (col.uiType || '-') + ' | db: ' + (col.dbType || '-')"
|
|
300
|
+
tooltipPosition="left">
|
|
301
|
+
<input type="checkbox" [(ngModel)]="col.selected"
|
|
302
|
+
[disabled]="col.virtual"
|
|
303
|
+
(ngModelChange)="toggleColumn(col, $event)" />
|
|
304
|
+
<i *ngIf="col.formula || col.formulaAlias" class="pi pi-bolt vb-node__formula-icon" title="Custom formula"></i>
|
|
305
|
+
<span [class.vb-node__col-fk]="col.uiType === 'lookupByID'"
|
|
306
|
+
[class.vb-node__col-formula]="col.formula || col.formulaAlias">{{ col.formulaAlias || col.label || col.alias }}<ng-container *ngIf="col.uiType === 'lookupByID' && col.alias !== col.label"> ({{ col.alias }})</ng-container></span>
|
|
307
|
+
<i *ngIf="col.uiType === 'lookupByID'" class="pi pi-link vb-node__fk-icon"
|
|
308
|
+
[title]="'FK → ' + col.lookupEntityName"></i>
|
|
309
|
+
</label>
|
|
310
|
+
<div class="vb-node__socket-out"
|
|
311
|
+
[class.vb-node__socket--connected]="$any(data).connectedOutputs?.has(col.alias)"
|
|
312
|
+
*ngIf="$any(data).outputs[col.alias]"
|
|
313
|
+
refComponent
|
|
314
|
+
[data]="{type: 'socket', side: 'output', key: col.alias, nodeId: data.id, payload: $any(data).outputs[col.alias]?.socket, seed: seed}"
|
|
315
|
+
[emit]="emit"></div>
|
|
316
|
+
<div class="vb-node__socket-in"
|
|
317
|
+
[class.vb-node__socket--connected]="$any(data).connectedInputs?.has(col.alias)"
|
|
318
|
+
*ngIf="$any(data).inputs[col.alias]"
|
|
319
|
+
refComponent
|
|
320
|
+
[data]="{type: 'socket', side: 'input', key: col.alias, nodeId: data.id, payload: $any(data).inputs[col.alias]?.socket, seed: seed}"
|
|
321
|
+
[emit]="emit"></div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
325
|
`, host: { 'data-testid': 'vb-node' }, styles: [":host{display:block}.vb-node{background:var(--surface-card, #fff);border:2px solid var(--surface-border, #dee2e6);border-radius:8px;min-width:240px;max-width:320px;font-size:.85rem;box-shadow:0 2px 8px #00000014;overflow:hidden}.vb-node--selected{border-color:var(--primary-color, #3B82F6)}.vb-node__header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--primary-color, #3B82F6);color:#fff;font-weight:600}.vb-node__alias{opacity:.7;font-size:.75rem}.vb-node__col-count{margin-left:auto;opacity:.8;font-size:.75rem}.vb-node__remove-btn{font-size:.7rem;opacity:.6;padding:2px 4px;cursor:pointer}.vb-node__remove-btn:hover{opacity:1}.vb-node__toggle-all{margin:0 4px 0 2px;cursor:pointer;vertical-align:middle}.vb-node__columns{max-height:260px;overflow-y:auto;padding:4px 0}.vb-node__col-row{display:flex;align-items:center;padding:2px 10px;position:relative;-webkit-user-select:none;user-select:none}.vb-node__col-row:hover{background:var(--surface-hover, #f4f4f4)}.vb-node__col-row--virtual{opacity:.45}.vb-node__col-row--has-formula{background:#eff6ff;border-left:2px solid var(--primary-color, #3B82F6)}.vb-node__col-row--virtual .vb-node__col-label{cursor:default}.vb-node__col-label{display:flex;align-items:center;gap:4px;flex:1;cursor:pointer;font-size:.8rem}.vb-node__col-fk{font-style:italic}.vb-node__col-formula{color:var(--primary-color, #3B82F6);font-weight:600}.vb-node__formula-icon{font-size:.65rem;color:#f59e0b;margin-right:1px}.vb-node__fk-icon{font-size:.7rem;color:var(--primary-color, #3B82F6)}.vb-node__socket-out,.vb-node__socket-in{width:16px;height:16px;border-radius:50%;border:2px solid var(--primary-color, #3B82F6);background:#fff;position:absolute;top:50%;transform:translateY(-50%);cursor:crosshair;z-index:10}.vb-node__socket-out{right:-8px}.vb-node__socket-out:hover{background:var(--primary-color, #3B82F6);border-color:#1d4ed8}.vb-node__socket-in{left:-8px}.vb-node__socket-in:hover{background:#4ade80;border-color:#16a34a}.vb-node__socket-out ::ng-deep>*,.vb-node__socket-in ::ng-deep>*{opacity:0;width:16px!important;height:16px!important;margin:0!important;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);cursor:crosshair}.vb-node__collapsed-sockets{position:relative;height:0;overflow:visible}.vb-node__socket-collapsed{position:absolute!important;top:-16px!important}.vb-node__socket-collapsed.vb-node__socket-out{right:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed.vb-node__socket-in{left:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed ::ng-deep>*{pointer-events:none!important}.vb-node__socket--connected ::ng-deep>*{opacity:1;background:#4ade80!important;border:2px solid #16a34a!important}\n"] }]
|
|
326
326
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { data: [{
|
|
327
327
|
type: Input
|
|
@@ -1914,11 +1914,11 @@ class ViewBuilderComponent {
|
|
|
1914
1914
|
}
|
|
1915
1915
|
}
|
|
1916
1916
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, deps: [{ token: i0.Injector }, { token: DataProviderService }, { token: MetadataProviderService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1917
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ViewBuilderComponent, isStandalone: true, selector: "wuic-view-builder", inputs: { autoReflowLayout: "autoReflowLayout" }, outputs: { viewDefinitionChange: "viewDefinitionChange" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["vbCanvas"], descendants: true }], ngImport: i0, template: "<div class=\"view-builder\">\n <!-- Palette: route search + drag -->\n <div class=\"view-builder__palette\">\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\n <div class=\"view-builder__palette-search\">\n <p-autoComplete\n [(ngModel)]=\"selectedRouteOption\"\n [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\"\n (onSelect)=\"selectedRouteOption = $event.value\"\n [dropdown]=\"true\"\n [forceSelection]=\"true\"\n optionLabel=\"label\"\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\n styleClass=\"w-full\">\n </p-autoComplete>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n [disabled]=\"!selectedRouteOption\"\n (onClick)=\"addSelectedRoute()\"\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\n </p-button>\n </div>\n @if (hasCanvasNodes) {\n <div class=\"view-builder__palette-filter\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\n {{ 'view_builder.show_related_only' | translate }}\n </label>\n </div>\n }\n <div class=\"view-builder__palette-list\">\n @for (opt of displayedRouteOptions; track opt.value) {\n <div class=\"view-builder__palette-item\"\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\n draggable=\"true\"\n (dragstart)=\"onPaletteDragStart($event, opt)\"\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\n <i class=\"pi pi-table\"></i>\n <span>{{ opt.label }}</span>\n @if (isRouteOnCanvas(opt.value)) {\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Canvas -->\n <div class=\"view-builder__canvas-area\">\n <div class=\"view-builder__canvas-toolbar\">\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\n </p-button>\n </div>\n <div #vbCanvas class=\"view-builder__canvas\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n (click)=\"hideContextMenu()\">\n </div>\n\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\n @if (hasCanvasNodes) {\n <div class=\"view-builder__aggregations-panel\">\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\n\n <!-- Form: add new aggregation -->\n <div class=\"view-builder__agg-form\">\n <p-select\n [(ngModel)]=\"newAggSourceColumn\"\n [options]=\"availableColumnsForAggregation\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\n </p-select>\n <p-select\n [(ngModel)]=\"newAggFunction\"\n [options]=\"aggregationFunctionOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\n </p-select>\n <input type=\"text\" class=\"vb-agg-alias-input\"\n [(ngModel)]=\"newAggOutputAlias\"\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addAggregation()\"\n [disabled]=\"!newAggSourceColumn\"\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\n </p-button>\n </div>\n\n <!-- Current aggregations list -->\n @if (aggregations.length > 0) {\n <div class=\"view-builder__agg-list\">\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\n <div class=\"view-builder__agg-item\">\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\n <span class=\"vb-agg-arrow\">\u2192</span>\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeAggregation(idx)\"\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\n </p-button>\n </div>\n }\n </div>\n }\n\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\n <div class=\"view-builder__groupby-section\">\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\n <p class=\"view-builder__groupby-hint\">\n {{ 'view_builder.group_by_hint' | translate }}\n </p>\n <div class=\"view-builder__groupby-list\">\n @for (col of availableGroupByColumns; track col.value) {\n <label class=\"view-builder__groupby-item\">\n <input type=\"checkbox\"\n [checked]=\"isInGroupBy(col.value)\"\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\n <span>{{ col.label }}</span>\n </label>\n }\n </div>\n </div>\n }\n\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\n <div class=\"view-builder__union-section\">\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\n\n <div class=\"view-builder__union-toolbar\">\n <label class=\"view-builder__union-mode\">\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\n </label>\n <p-select\n [(ngModel)]=\"newUnionBlockRoute\"\n [options]=\"routeOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\n </p-select>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addUnionBlock()\"\n [disabled]=\"!newUnionBlockRoute\"\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\n </p-button>\n </div>\n\n @if (unionBlocks.length > 0) {\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\n <div class=\"view-builder__union-block\">\n <div class=\"view-builder__union-block-header\">\n <span class=\"vb-union-block-title\">\n <i class=\"pi pi-link\"></i>\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\n </span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeUnionBlock(bidx)\"\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\n </p-button>\n </div>\n\n <div class=\"view-builder__union-mapping\">\n <div class=\"vb-union-mapping-header\">\n {{ 'view_builder.column_mapping_heading' | translate }}\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\n </div>\n @for (m of block.columnMapping; track m.canonicalAlias) {\n <div class=\"vb-union-mapping-row\">\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\n <span class=\"vb-mapping-arrow\">\u2190</span>\n <p-select\n [(ngModel)]=\"m.sourceQualified\"\n [options]=\"blockColumnOptions(block)\"\n optionLabel=\"label\" optionValue=\"value\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\n </p-select>\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\n <input type=\"text\" class=\"vb-mapping-constant\"\n [(ngModel)]=\"m.constantValue\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Connection context menu (join type) -->\n @if (contextMenuVisible) {\n <div class=\"vb-context-menu\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n @for (opt of joinTypeOptions; track opt.value) {\n <div class=\"vb-context-menu__item\"\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\n (click)=\"setJoinType(opt.value)\">\n {{ opt.label }}\n </div>\n }\n <div class=\"vb-context-menu__separator\"></div>\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\n (click)=\"removeConnection()\">\n {{ 'view_builder.remove_join' | translate }}\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2.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: "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: SelectModule }, { kind: "component", type: i5.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: TooltipModule }, { kind: "directive", type: i7.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
|
|
1917
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ViewBuilderComponent, isStandalone: true, selector: "wuic-view-builder", inputs: { autoReflowLayout: "autoReflowLayout" }, outputs: { viewDefinitionChange: "viewDefinitionChange" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["vbCanvas"], descendants: true }], ngImport: i0, template: "<div class=\"view-builder\">\r\n <!-- Palette: route search + drag -->\r\n <div class=\"view-builder__palette\">\r\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\r\n <div class=\"view-builder__palette-search\">\r\n <p-autoComplete\r\n [(ngModel)]=\"selectedRouteOption\"\r\n [suggestions]=\"filteredRouteOptions\"\r\n (completeMethod)=\"filterRouteOptions($event)\"\r\n (onSelect)=\"selectedRouteOption = $event.value\"\r\n [dropdown]=\"true\"\r\n [forceSelection]=\"true\"\r\n optionLabel=\"label\"\r\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\r\n styleClass=\"w-full\">\r\n </p-autoComplete>\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n [disabled]=\"!selectedRouteOption\"\r\n (onClick)=\"addSelectedRoute()\"\r\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\r\n </p-button>\r\n </div>\r\n @if (hasCanvasNodes) {\r\n <div class=\"view-builder__palette-filter\">\r\n <label>\r\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\r\n {{ 'view_builder.show_related_only' | translate }}\r\n </label>\r\n </div>\r\n }\r\n <div class=\"view-builder__palette-list\">\r\n @for (opt of displayedRouteOptions; track opt.value) {\r\n <div class=\"view-builder__palette-item\"\r\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\r\n draggable=\"true\"\r\n (dragstart)=\"onPaletteDragStart($event, opt)\"\r\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\r\n <i class=\"pi pi-table\"></i>\r\n <span>{{ opt.label }}</span>\r\n @if (isRouteOnCanvas(opt.value)) {\r\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Canvas -->\r\n <div class=\"view-builder__canvas-area\">\r\n <div class=\"view-builder__canvas-toolbar\">\r\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\r\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\r\n </p-button>\r\n </div>\r\n <div #vbCanvas class=\"view-builder__canvas\"\r\n (dragover)=\"onCanvasDragOver($event)\"\r\n (drop)=\"onCanvasDrop($event)\"\r\n (click)=\"hideContextMenu()\">\r\n </div>\r\n\r\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\r\n @if (hasCanvasNodes) {\r\n <div class=\"view-builder__aggregations-panel\">\r\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\r\n\r\n <!-- Form: add new aggregation -->\r\n <div class=\"view-builder__agg-form\">\r\n <p-select\r\n [(ngModel)]=\"newAggSourceColumn\"\r\n [options]=\"availableColumnsForAggregation\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\r\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\r\n </p-select>\r\n <p-select\r\n [(ngModel)]=\"newAggFunction\"\r\n [options]=\"aggregationFunctionOptions\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\r\n </p-select>\r\n <input type=\"text\" class=\"vb-agg-alias-input\"\r\n [(ngModel)]=\"newAggOutputAlias\"\r\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n (onClick)=\"addAggregation()\"\r\n [disabled]=\"!newAggSourceColumn\"\r\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\r\n </p-button>\r\n </div>\r\n\r\n <!-- Current aggregations list -->\r\n @if (aggregations.length > 0) {\r\n <div class=\"view-builder__agg-list\">\r\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\r\n <div class=\"view-builder__agg-item\">\r\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\r\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\r\n <span class=\"vb-agg-arrow\">\u2192</span>\r\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\r\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\r\n (onClick)=\"removeAggregation(idx)\"\r\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\r\n </p-button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\r\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\r\n <div class=\"view-builder__groupby-section\">\r\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\r\n <p class=\"view-builder__groupby-hint\">\r\n {{ 'view_builder.group_by_hint' | translate }}\r\n </p>\r\n <div class=\"view-builder__groupby-list\">\r\n @for (col of availableGroupByColumns; track col.value) {\r\n <label class=\"view-builder__groupby-item\">\r\n <input type=\"checkbox\"\r\n [checked]=\"isInGroupBy(col.value)\"\r\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\r\n <span>{{ col.label }}</span>\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\r\n <div class=\"view-builder__union-section\">\r\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\r\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\r\n\r\n <div class=\"view-builder__union-toolbar\">\r\n <label class=\"view-builder__union-mode\">\r\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\r\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\r\n </label>\r\n <p-select\r\n [(ngModel)]=\"newUnionBlockRoute\"\r\n [options]=\"routeOptions\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\r\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\r\n </p-select>\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n (onClick)=\"addUnionBlock()\"\r\n [disabled]=\"!newUnionBlockRoute\"\r\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\r\n </p-button>\r\n </div>\r\n\r\n @if (unionBlocks.length > 0) {\r\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\r\n <div class=\"view-builder__union-block\">\r\n <div class=\"view-builder__union-block-header\">\r\n <span class=\"vb-union-block-title\">\r\n <i class=\"pi pi-link\"></i>\r\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\r\n </span>\r\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\r\n (onClick)=\"removeUnionBlock(bidx)\"\r\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\r\n </p-button>\r\n </div>\r\n\r\n <div class=\"view-builder__union-mapping\">\r\n <div class=\"vb-union-mapping-header\">\r\n {{ 'view_builder.column_mapping_heading' | translate }}\r\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\r\n </div>\r\n @for (m of block.columnMapping; track m.canonicalAlias) {\r\n <div class=\"vb-union-mapping-row\">\r\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\r\n <span class=\"vb-mapping-arrow\">\u2190</span>\r\n <p-select\r\n [(ngModel)]=\"m.sourceQualified\"\r\n [options]=\"blockColumnOptions(block)\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n (ngModelChange)=\"onUnionMappingChanged()\"\r\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\r\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\r\n </p-select>\r\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\r\n <input type=\"text\" class=\"vb-mapping-constant\"\r\n [(ngModel)]=\"m.constantValue\"\r\n (ngModelChange)=\"onUnionMappingChanged()\"\r\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connection context menu (join type) -->\r\n @if (contextMenuVisible) {\r\n <div class=\"vb-context-menu\"\r\n [style.left.px]=\"contextMenuX\"\r\n [style.top.px]=\"contextMenuY\">\r\n @for (opt of joinTypeOptions; track opt.value) {\r\n <div class=\"vb-context-menu__item\"\r\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\r\n (click)=\"setJoinType(opt.value)\">\r\n {{ opt.label }}\r\n </div>\r\n }\r\n <div class=\"vb-context-menu__separator\"></div>\r\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\r\n (click)=\"removeConnection()\">\r\n {{ 'view_builder.remove_join' | translate }}\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2.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: "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: SelectModule }, { kind: "component", type: i5.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: TooltipModule }, { kind: "directive", type: i7.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
|
|
1918
1918
|
}
|
|
1919
1919
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, decorators: [{
|
|
1920
1920
|
type: Component,
|
|
1921
|
-
args: [{ selector: 'wuic-view-builder', standalone: true, imports: [FormsModule, AutoCompleteModule, ButtonModule, SelectModule, TooltipModule, TranslateModule], template: "<div class=\"view-builder\">\n <!-- Palette: route search + drag -->\n <div class=\"view-builder__palette\">\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\n <div class=\"view-builder__palette-search\">\n <p-autoComplete\n [(ngModel)]=\"selectedRouteOption\"\n [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\"\n (onSelect)=\"selectedRouteOption = $event.value\"\n [dropdown]=\"true\"\n [forceSelection]=\"true\"\n optionLabel=\"label\"\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\n styleClass=\"w-full\">\n </p-autoComplete>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n [disabled]=\"!selectedRouteOption\"\n (onClick)=\"addSelectedRoute()\"\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\n </p-button>\n </div>\n @if (hasCanvasNodes) {\n <div class=\"view-builder__palette-filter\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\n {{ 'view_builder.show_related_only' | translate }}\n </label>\n </div>\n }\n <div class=\"view-builder__palette-list\">\n @for (opt of displayedRouteOptions; track opt.value) {\n <div class=\"view-builder__palette-item\"\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\n draggable=\"true\"\n (dragstart)=\"onPaletteDragStart($event, opt)\"\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\n <i class=\"pi pi-table\"></i>\n <span>{{ opt.label }}</span>\n @if (isRouteOnCanvas(opt.value)) {\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Canvas -->\n <div class=\"view-builder__canvas-area\">\n <div class=\"view-builder__canvas-toolbar\">\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\n </p-button>\n </div>\n <div #vbCanvas class=\"view-builder__canvas\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n (click)=\"hideContextMenu()\">\n </div>\n\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\n @if (hasCanvasNodes) {\n <div class=\"view-builder__aggregations-panel\">\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\n\n <!-- Form: add new aggregation -->\n <div class=\"view-builder__agg-form\">\n <p-select\n [(ngModel)]=\"newAggSourceColumn\"\n [options]=\"availableColumnsForAggregation\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\n </p-select>\n <p-select\n [(ngModel)]=\"newAggFunction\"\n [options]=\"aggregationFunctionOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\n </p-select>\n <input type=\"text\" class=\"vb-agg-alias-input\"\n [(ngModel)]=\"newAggOutputAlias\"\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addAggregation()\"\n [disabled]=\"!newAggSourceColumn\"\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\n </p-button>\n </div>\n\n <!-- Current aggregations list -->\n @if (aggregations.length > 0) {\n <div class=\"view-builder__agg-list\">\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\n <div class=\"view-builder__agg-item\">\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\n <span class=\"vb-agg-arrow\">\u2192</span>\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeAggregation(idx)\"\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\n </p-button>\n </div>\n }\n </div>\n }\n\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\n <div class=\"view-builder__groupby-section\">\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\n <p class=\"view-builder__groupby-hint\">\n {{ 'view_builder.group_by_hint' | translate }}\n </p>\n <div class=\"view-builder__groupby-list\">\n @for (col of availableGroupByColumns; track col.value) {\n <label class=\"view-builder__groupby-item\">\n <input type=\"checkbox\"\n [checked]=\"isInGroupBy(col.value)\"\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\n <span>{{ col.label }}</span>\n </label>\n }\n </div>\n </div>\n }\n\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\n <div class=\"view-builder__union-section\">\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\n\n <div class=\"view-builder__union-toolbar\">\n <label class=\"view-builder__union-mode\">\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\n </label>\n <p-select\n [(ngModel)]=\"newUnionBlockRoute\"\n [options]=\"routeOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\n </p-select>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addUnionBlock()\"\n [disabled]=\"!newUnionBlockRoute\"\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\n </p-button>\n </div>\n\n @if (unionBlocks.length > 0) {\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\n <div class=\"view-builder__union-block\">\n <div class=\"view-builder__union-block-header\">\n <span class=\"vb-union-block-title\">\n <i class=\"pi pi-link\"></i>\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\n </span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeUnionBlock(bidx)\"\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\n </p-button>\n </div>\n\n <div class=\"view-builder__union-mapping\">\n <div class=\"vb-union-mapping-header\">\n {{ 'view_builder.column_mapping_heading' | translate }}\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\n </div>\n @for (m of block.columnMapping; track m.canonicalAlias) {\n <div class=\"vb-union-mapping-row\">\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\n <span class=\"vb-mapping-arrow\">\u2190</span>\n <p-select\n [(ngModel)]=\"m.sourceQualified\"\n [options]=\"blockColumnOptions(block)\"\n optionLabel=\"label\" optionValue=\"value\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\n </p-select>\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\n <input type=\"text\" class=\"vb-mapping-constant\"\n [(ngModel)]=\"m.constantValue\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Connection context menu (join type) -->\n @if (contextMenuVisible) {\n <div class=\"vb-context-menu\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n @for (opt of joinTypeOptions; track opt.value) {\n <div class=\"vb-context-menu__item\"\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\n (click)=\"setJoinType(opt.value)\">\n {{ opt.label }}\n </div>\n }\n <div class=\"vb-context-menu__separator\"></div>\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\n (click)=\"removeConnection()\">\n {{ 'view_builder.remove_join' | translate }}\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"] }]
|
|
1921
|
+
args: [{ selector: 'wuic-view-builder', standalone: true, imports: [FormsModule, AutoCompleteModule, ButtonModule, SelectModule, TooltipModule, TranslateModule], template: "<div class=\"view-builder\">\r\n <!-- Palette: route search + drag -->\r\n <div class=\"view-builder__palette\">\r\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\r\n <div class=\"view-builder__palette-search\">\r\n <p-autoComplete\r\n [(ngModel)]=\"selectedRouteOption\"\r\n [suggestions]=\"filteredRouteOptions\"\r\n (completeMethod)=\"filterRouteOptions($event)\"\r\n (onSelect)=\"selectedRouteOption = $event.value\"\r\n [dropdown]=\"true\"\r\n [forceSelection]=\"true\"\r\n optionLabel=\"label\"\r\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\r\n styleClass=\"w-full\">\r\n </p-autoComplete>\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n [disabled]=\"!selectedRouteOption\"\r\n (onClick)=\"addSelectedRoute()\"\r\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\r\n </p-button>\r\n </div>\r\n @if (hasCanvasNodes) {\r\n <div class=\"view-builder__palette-filter\">\r\n <label>\r\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\r\n {{ 'view_builder.show_related_only' | translate }}\r\n </label>\r\n </div>\r\n }\r\n <div class=\"view-builder__palette-list\">\r\n @for (opt of displayedRouteOptions; track opt.value) {\r\n <div class=\"view-builder__palette-item\"\r\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\r\n draggable=\"true\"\r\n (dragstart)=\"onPaletteDragStart($event, opt)\"\r\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\r\n <i class=\"pi pi-table\"></i>\r\n <span>{{ opt.label }}</span>\r\n @if (isRouteOnCanvas(opt.value)) {\r\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Canvas -->\r\n <div class=\"view-builder__canvas-area\">\r\n <div class=\"view-builder__canvas-toolbar\">\r\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\r\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\r\n </p-button>\r\n </div>\r\n <div #vbCanvas class=\"view-builder__canvas\"\r\n (dragover)=\"onCanvasDragOver($event)\"\r\n (drop)=\"onCanvasDrop($event)\"\r\n (click)=\"hideContextMenu()\">\r\n </div>\r\n\r\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\r\n @if (hasCanvasNodes) {\r\n <div class=\"view-builder__aggregations-panel\">\r\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\r\n\r\n <!-- Form: add new aggregation -->\r\n <div class=\"view-builder__agg-form\">\r\n <p-select\r\n [(ngModel)]=\"newAggSourceColumn\"\r\n [options]=\"availableColumnsForAggregation\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\r\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\r\n </p-select>\r\n <p-select\r\n [(ngModel)]=\"newAggFunction\"\r\n [options]=\"aggregationFunctionOptions\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\r\n </p-select>\r\n <input type=\"text\" class=\"vb-agg-alias-input\"\r\n [(ngModel)]=\"newAggOutputAlias\"\r\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n (onClick)=\"addAggregation()\"\r\n [disabled]=\"!newAggSourceColumn\"\r\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\r\n </p-button>\r\n </div>\r\n\r\n <!-- Current aggregations list -->\r\n @if (aggregations.length > 0) {\r\n <div class=\"view-builder__agg-list\">\r\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\r\n <div class=\"view-builder__agg-item\">\r\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\r\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\r\n <span class=\"vb-agg-arrow\">\u2192</span>\r\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\r\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\r\n (onClick)=\"removeAggregation(idx)\"\r\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\r\n </p-button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\r\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\r\n <div class=\"view-builder__groupby-section\">\r\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\r\n <p class=\"view-builder__groupby-hint\">\r\n {{ 'view_builder.group_by_hint' | translate }}\r\n </p>\r\n <div class=\"view-builder__groupby-list\">\r\n @for (col of availableGroupByColumns; track col.value) {\r\n <label class=\"view-builder__groupby-item\">\r\n <input type=\"checkbox\"\r\n [checked]=\"isInGroupBy(col.value)\"\r\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\r\n <span>{{ col.label }}</span>\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\r\n <div class=\"view-builder__union-section\">\r\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\r\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\r\n\r\n <div class=\"view-builder__union-toolbar\">\r\n <label class=\"view-builder__union-mode\">\r\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\r\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\r\n </label>\r\n <p-select\r\n [(ngModel)]=\"newUnionBlockRoute\"\r\n [options]=\"routeOptions\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\r\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\r\n </p-select>\r\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\r\n (onClick)=\"addUnionBlock()\"\r\n [disabled]=\"!newUnionBlockRoute\"\r\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\r\n </p-button>\r\n </div>\r\n\r\n @if (unionBlocks.length > 0) {\r\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\r\n <div class=\"view-builder__union-block\">\r\n <div class=\"view-builder__union-block-header\">\r\n <span class=\"vb-union-block-title\">\r\n <i class=\"pi pi-link\"></i>\r\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\r\n </span>\r\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\r\n (onClick)=\"removeUnionBlock(bidx)\"\r\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\r\n </p-button>\r\n </div>\r\n\r\n <div class=\"view-builder__union-mapping\">\r\n <div class=\"vb-union-mapping-header\">\r\n {{ 'view_builder.column_mapping_heading' | translate }}\r\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\r\n </div>\r\n @for (m of block.columnMapping; track m.canonicalAlias) {\r\n <div class=\"vb-union-mapping-row\">\r\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\r\n <span class=\"vb-mapping-arrow\">\u2190</span>\r\n <p-select\r\n [(ngModel)]=\"m.sourceQualified\"\r\n [options]=\"blockColumnOptions(block)\"\r\n optionLabel=\"label\" optionValue=\"value\"\r\n (ngModelChange)=\"onUnionMappingChanged()\"\r\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\r\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\r\n </p-select>\r\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\r\n <input type=\"text\" class=\"vb-mapping-constant\"\r\n [(ngModel)]=\"m.constantValue\"\r\n (ngModelChange)=\"onUnionMappingChanged()\"\r\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connection context menu (join type) -->\r\n @if (contextMenuVisible) {\r\n <div class=\"vb-context-menu\"\r\n [style.left.px]=\"contextMenuX\"\r\n [style.top.px]=\"contextMenuY\">\r\n @for (opt of joinTypeOptions; track opt.value) {\r\n <div class=\"vb-context-menu__item\"\r\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\r\n (click)=\"setJoinType(opt.value)\">\r\n {{ opt.label }}\r\n </div>\r\n }\r\n <div class=\"vb-context-menu__separator\"></div>\r\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\r\n (click)=\"removeConnection()\">\r\n {{ 'view_builder.remove_join' | translate }}\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"] }]
|
|
1922
1922
|
}], ctorParameters: () => [{ type: i0.Injector }, { type: DataProviderService }, { type: MetadataProviderService }], propDecorators: { viewDefinitionChange: [{
|
|
1923
1923
|
type: Output
|
|
1924
1924
|
}], autoReflowLayout: [{
|
|
@@ -4936,11 +4936,11 @@ class PivotBuilderComponent {
|
|
|
4936
4936
|
return axis.map((c) => c.dateGroupBy ? `${c.alias}(groupBy=${c.dateGroupBy})` : c.alias).join(', ');
|
|
4937
4937
|
}
|
|
4938
4938
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: TranslationManagerService }, { token: i1$2.ActivatedRoute }, { token: LicenseFeatureService }, { token: ChatbotHostRegistryService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4939
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PivotBuilderComponent, isStandalone: true, selector: "wuic-pivot-builder", viewQueries: [{ propertyName: "nestedSource", first: true, predicate: ["nestedSource"], descendants: true }, { propertyName: "viewBuilderRef", first: true, predicate: ["viewBuilderRef"], descendants: true }], ngImport: i0, template: "<div class=\"pivot-builder\">\n <!-- ===== Global header (just title) ===== -->\n <div class=\"pivot-toolbar\">\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\n </div>\n\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\n </wuic-data-source>\n\n <!-- ===== Tabs ===== -->\n <p-tabs [(value)]=\"activeTab\">\n <p-tablist>\n <p-tab [value]=\"0\">\n <i class=\"pi pi-sitemap mr-2\"></i>\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\n </p-tab>\n <p-tab [value]=\"1\">\n <i class=\"pi pi-th-large mr-2\"></i>\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\n </p-tab>\n </p-tablist>\n <p-tabpanels>\n\n <!-- ==================== Tab 0: View Builder ==================== -->\n <p-tabpanel [value]=\"0\">\n <!-- View Builder toolbar -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar: tutti i button uniformati al tema selezionato\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\n Rimosse severity override per evitare mix `contrast/success/\n secondary` che rompeva la coerenza visiva.\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\n raggruppati nello split-button \"Definition\". -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\n </p-button>\n <p-splitButton #definitionSplitBtn\n [label]=\"'pivot_builder.vb.definition' | translate\"\n icon=\"pi pi-database\"\n [model]=\"definitionMenuItems\"\n styleClass=\"pivot-actions-definition-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\n </p-splitButton>\n </div>\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\n tab, pattern standard del framework \u2014 vedi list-grid.component\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\n span semplice (es. view creata senza scaffold, o payload\n legacy senza route). -->\n @if (createdViewName) {\n @if (createdViewRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + createdViewRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n </span>\n }\n }\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\n del menu e' un toggle con icona check/stop che riflette lo\n stato corrente (auto-generate, manual SQL, all-filterable,\n auto-reflow). Click sulla voce inverte il flag.\n Allineato a destra della toolbar tramite\n `.pivot-actions-settings` (margin-left: auto). -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #settingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"settingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\n <div class=\"pivot-filter-full\">\n <wuic-filter-bar\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\n [skipUrlSync]=\"true\"\n [skipFetch]=\"true\"\n (filterApplied)=\"onViewBuilderFilterApplied()\"\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\n </wuic-filter-bar>\n </div>\n }\n\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\n </wuic-view-builder>\n </div>\n </p-tabpanel>\n\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\n <p-tabpanel [value]=\"1\">\n <!-- Pivot toolbar: route selector + actions -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\n tab View Builder: nessuna severity override).\n \"Create View\", \"Create Materialized Table\",\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\n split-button \"Actions\" (stesso pattern del Definition\n split-button del tab View Builder). -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\n </p-button>\n <p-splitButton #pivotConfigSplitBtn\n [label]=\"'pivot_builder.actions' | translate\"\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\n ? 'pi pi-spin pi-spinner'\n : 'pi pi-ellipsis-v'\"\n [model]=\"pivotConfigMenuItems\"\n styleClass=\"pivot-actions-config-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\n </p-splitButton>\n <!-- Chip cliccabile della view/materialized-table scaffoldata\n dal pivot (pattern identico al chip createdViewName del\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\n Tooltip include il tipo (view o table) serializzato. -->\n @if (pivotCreatedName) {\n @if (pivotCreatedRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"(pivotCreatedKind === 'table'\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n </span>\n }\n }\n </div>\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\n del View Builder Settings (auto-generate, manual SQL,\n all-filterable) meno il `auto-reflow` che e' specifico del\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #pivotSettingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"pivotSettingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <table style=\"width: 100%;\">\n <tr>\n <td>\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\n styleClass=\"pivot-route-autocomplete\">\n </p-autoComplete>\n </td>\n <td>\n <label>{{ 'aggregation' | translate }}</label>\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"aggregateFunction\">\n </p-select>\n </td>\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\n il pageSize non e' stato applicato esplicitamente. -->\n </tr>\n </table>\n\n <!-- Filter panel -->\n <div class=\"pivot-filter-full\">\n\n @if (selectedRouteName) {\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\n (filterApplied)=\"onFilterBarApplied()\"\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\n }\n\n </div>\n\n <!-- 4-panel splitter: source cols | rows | columns | values -->\n <div class=\"pivot-buckets-row\">\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\n styleClass=\"pivot-buckets-splitter\">\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\n @for (col of sourceColumns; track col.alias) {\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\n <span>{{ col.label }}</span>\n <div class=\"pivot-column-actions\">\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\n }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\n }}</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\n @if (!rowColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\n }\n @for (col of rowColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\n @if (!columnColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\n }\n @for (col of columnColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\n @if (!valueColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\n }\n @for (col of valueColumns; track col.id) {\n <div class=\"pivot-chip\">\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\n <div class=\"pivot-chip-actions\">\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n </p-splitter>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n\n <!-- ===== Output row (below tabs, always visible) ===== -->\n <div class=\"pivot-output-row\">\n @if (!showQueryResults) {\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n } @else {\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\n <ng-template #panel>\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n </ng-template>\n <ng-template #panel>\n <div class=\"pivot-result-panel\">\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\n <ng-template pTemplate=\"header\">\n <tr>\n @for (c of queryResultColumns; track c) {\n <th>{{ c }}</th>\n }\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-row>\n <tr>\n @for (c of queryResultColumns; track c) {\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\n }\n </tr>\n </ng-template>\n </p-table>\n </div>\n </ng-template>\n </p-splitter>\n }\n </div>\n</div>\n\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenPivotDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [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)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2.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: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.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: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2$1.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: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "component", type: Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: TabList, selector: "p-tablist" }, { kind: "component", type: Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: TabPanels, selector: "p-tabpanels" }, { kind: "component", type: TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "ngmodule", type: MonacoEditorModule }, { kind: "component", type: i1$3.MonacoEditorComponent, selector: "ngx-monaco-editor", inputs: ["options", "uri"], outputs: ["init"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: DataSourceComponent, selector: "wuic-data-source", inputs: ["route", "routeFromRouting", "hardcodedRoute", "autoload", "loading", "changeTracking", "parentRecord", "parentMetaInfo", "parentDatasource", "useComboEndpoint", "comboFormulaLookup", "comboMcId", "comboField", "comboExtraFields", "comboCurrentRecord", "componentRef"] }, { kind: "component", type: FilterBarComponent, selector: "wuic-filter-bar", inputs: ["datasource", "hardcodedDatasource", "multiDatasourceMetas", "skipUrlSync", "skipFetch"], outputs: ["filterApplied", "onFilterApplied", "onSortingApplied", "onPageSizeApplied", "onGroupingChanged", "onFilterBarCollapseChanged"] }, { kind: "component", type: ViewBuilderComponent, selector: "wuic-view-builder", inputs: ["autoReflowLayout"], outputs: ["viewDefinitionChange"] }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
|
|
4939
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PivotBuilderComponent, isStandalone: true, selector: "wuic-pivot-builder", viewQueries: [{ propertyName: "nestedSource", first: true, predicate: ["nestedSource"], descendants: true }, { propertyName: "viewBuilderRef", first: true, predicate: ["viewBuilderRef"], descendants: true }], ngImport: i0, template: "<div class=\"pivot-builder\">\r\n <!-- ===== Global header (just title) ===== -->\r\n <div class=\"pivot-toolbar\">\r\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\r\n </div>\r\n\r\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\r\n </wuic-data-source>\r\n\r\n <!-- ===== Tabs ===== -->\r\n <p-tabs [(value)]=\"activeTab\">\r\n <p-tablist>\r\n <p-tab [value]=\"0\">\r\n <i class=\"pi pi-sitemap mr-2\"></i>\r\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\r\n </p-tab>\r\n <p-tab [value]=\"1\">\r\n <i class=\"pi pi-th-large mr-2\"></i>\r\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\r\n </p-tab>\r\n </p-tablist>\r\n <p-tabpanels>\r\n\r\n <!-- ==================== Tab 0: View Builder ==================== -->\r\n <p-tabpanel [value]=\"0\">\r\n <!-- View Builder toolbar -->\r\n <div class=\"pivot-actions-row\">\r\n <!-- Toolbar: tutti i button uniformati al tema selezionato\r\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\r\n Rimosse severity override per evitare mix `contrast/success/\r\n secondary` che rompeva la coerenza visiva.\r\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\r\n raggruppati nello split-button \"Definition\". -->\r\n <div class=\"pivot-actions\">\r\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\r\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\r\n </p-button>\r\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\r\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\r\n </p-button>\r\n <p-splitButton #definitionSplitBtn\r\n [label]=\"'pivot_builder.vb.definition' | translate\"\r\n icon=\"pi pi-database\"\r\n [model]=\"definitionMenuItems\"\r\n styleClass=\"pivot-actions-definition-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\r\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\r\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\r\n tab, pattern standard del framework \u2014 vedi list-grid.component\r\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\r\n span semplice (es. view creata senza scaffold, o payload\r\n legacy senza route). -->\r\n @if (createdViewName) {\r\n @if (createdViewRoute) {\r\n <a class=\"pivot-created-view-label pivot-created-view-link\"\r\n [href]=\"'#/' + createdViewRoute + '/list'\"\r\n target=\"_blank\" rel=\"noopener\"\r\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\r\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\r\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\r\n </a>\r\n } @else {\r\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\r\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\r\n </span>\r\n }\r\n }\r\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\r\n del menu e' un toggle con icona check/stop che riflette lo\r\n stato corrente (auto-generate, manual SQL, all-filterable,\r\n auto-reflow). Click sulla voce inverte il flag.\r\n Allineato a destra della toolbar tramite\r\n `.pivot-actions-settings` (margin-left: auto). -->\r\n <div class=\"pivot-actions-settings\">\r\n <p-splitButton #settingsSplitBtn\r\n [label]=\"'pivot_builder.vb.settings' | translate\"\r\n icon=\"pi pi-cog\"\r\n [model]=\"settingsMenuItems\"\r\n styleClass=\"pivot-actions-settings-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\r\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n </div>\r\n\r\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\r\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\r\n <div class=\"pivot-filter-full\">\r\n <wuic-filter-bar\r\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\r\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\r\n [skipUrlSync]=\"true\"\r\n [skipFetch]=\"true\"\r\n (filterApplied)=\"onViewBuilderFilterApplied()\"\r\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\r\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\r\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\r\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\r\n </wuic-filter-bar>\r\n </div>\r\n }\r\n\r\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\r\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\r\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\r\n </wuic-view-builder>\r\n </div>\r\n </p-tabpanel>\r\n\r\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\r\n <p-tabpanel [value]=\"1\">\r\n <!-- Pivot toolbar: route selector + actions -->\r\n <div class=\"pivot-actions-row\">\r\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\r\n tab View Builder: nessuna severity override).\r\n \"Create View\", \"Create Materialized Table\",\r\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\r\n split-button \"Actions\" (stesso pattern del Definition\r\n split-button del tab View Builder). -->\r\n <div class=\"pivot-actions\">\r\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\r\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\r\n </p-button>\r\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\r\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\r\n </p-button>\r\n <p-splitButton #pivotConfigSplitBtn\r\n [label]=\"'pivot_builder.actions' | translate\"\r\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\r\n ? 'pi pi-spin pi-spinner'\r\n : 'pi pi-ellipsis-v'\"\r\n [model]=\"pivotConfigMenuItems\"\r\n styleClass=\"pivot-actions-config-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\r\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\r\n </p-splitButton>\r\n <!-- Chip cliccabile della view/materialized-table scaffoldata\r\n dal pivot (pattern identico al chip createdViewName del\r\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\r\n Tooltip include il tipo (view o table) serializzato. -->\r\n @if (pivotCreatedName) {\r\n @if (pivotCreatedRoute) {\r\n <a class=\"pivot-created-view-label pivot-created-view-link\"\r\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\r\n target=\"_blank\" rel=\"noopener\"\r\n [title]=\"(pivotCreatedKind === 'table'\r\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\r\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\r\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\r\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\r\n {{ pivotCreatedName }}\r\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\r\n </a>\r\n } @else {\r\n <span class=\"pivot-created-view-label\">\r\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\r\n {{ pivotCreatedName }}\r\n </span>\r\n }\r\n }\r\n </div>\r\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\r\n del View Builder Settings (auto-generate, manual SQL,\r\n all-filterable) meno il `auto-reflow` che e' specifico del\r\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\r\n <div class=\"pivot-actions-settings\">\r\n <p-splitButton #pivotSettingsSplitBtn\r\n [label]=\"'pivot_builder.vb.settings' | translate\"\r\n icon=\"pi pi-cog\"\r\n [model]=\"pivotSettingsMenuItems\"\r\n styleClass=\"pivot-actions-settings-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\r\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n </div>\r\n\r\n <table style=\"width: 100%;\">\r\n <tr>\r\n <td>\r\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\r\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\r\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\r\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\r\n styleClass=\"pivot-route-autocomplete\">\r\n </p-autoComplete>\r\n </td>\r\n <td>\r\n <label>{{ 'aggregation' | translate }}</label>\r\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [(ngModel)]=\"aggregateFunction\">\r\n </p-select>\r\n </td>\r\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\r\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\r\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\r\n il pageSize non e' stato applicato esplicitamente. -->\r\n </tr>\r\n </table>\r\n\r\n <!-- Filter panel -->\r\n <div class=\"pivot-filter-full\">\r\n\r\n @if (selectedRouteName) {\r\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\r\n (filterApplied)=\"onFilterBarApplied()\"\r\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\r\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\r\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\r\n }\r\n\r\n </div>\r\n\r\n <!-- 4-panel splitter: source cols | rows | columns | values -->\r\n <div class=\"pivot-buckets-row\">\r\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\r\n styleClass=\"pivot-buckets-splitter\">\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\r\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\r\n @for (col of sourceColumns; track col.alias) {\r\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\r\n <span>{{ col.label }}</span>\r\n <div class=\"pivot-column-actions\">\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\r\n }}</button>\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\r\n }}</button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\r\n @if (!rowColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\r\n }\r\n @for (col of rowColumns; track col.alias) {\r\n <div class=\"pivot-chip\">\r\n <span>{{ col.label }}</span>\r\n @if (isAxisDateCastAvailable(col)) {\r\n <label class=\"pivot-row-cast-flag\">\r\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\r\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\r\n {{ 'pivot_builder.cast_date' | translate }}\r\n </label>\r\n }\r\n @if (isAxisDateTimeGroupAvailable(col)) {\r\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n }\r\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\r\n @if (!columnColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\r\n }\r\n @for (col of columnColumns; track col.alias) {\r\n <div class=\"pivot-chip\">\r\n <span>{{ col.label }}</span>\r\n @if (isAxisDateCastAvailable(col)) {\r\n <label class=\"pivot-row-cast-flag\">\r\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\r\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\r\n {{ 'pivot_builder.cast_date' | translate }}\r\n </label>\r\n }\r\n @if (isAxisDateTimeGroupAvailable(col)) {\r\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n }\r\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\r\n @if (!valueColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\r\n }\r\n @for (col of valueColumns; track col.id) {\r\n <div class=\"pivot-chip\">\r\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\r\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\r\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\r\n <div class=\"pivot-chip-actions\">\r\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n </p-splitter>\r\n </div>\r\n </p-tabpanel>\r\n </p-tabpanels>\r\n </p-tabs>\r\n\r\n <!-- ===== Output row (below tabs, always visible) ===== -->\r\n <div class=\"pivot-output-row\">\r\n @if (!showQueryResults) {\r\n <div class=\"pivot-sql-editor\">\r\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\r\n @if (generatingViewDefSql || generatingSql) {\r\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\r\n }\r\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\r\n </div>\r\n } @else {\r\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\r\n <ng-template #panel>\r\n <div class=\"pivot-sql-editor\">\r\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\r\n @if (generatingViewDefSql || generatingSql) {\r\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\r\n }\r\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\r\n </div>\r\n </ng-template>\r\n <ng-template #panel>\r\n <div class=\"pivot-result-panel\">\r\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\r\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\r\n <ng-template pTemplate=\"header\">\r\n <tr>\r\n @for (c of queryResultColumns; track c) {\r\n <th>{{ c }}</th>\r\n }\r\n </tr>\r\n </ng-template>\r\n <ng-template pTemplate=\"body\" let-row>\r\n <tr>\r\n @for (c of queryResultColumns; track c) {\r\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\r\n }\r\n </tr>\r\n </ng-template>\r\n </p-table>\r\n </div>\r\n </ng-template>\r\n </p-splitter>\r\n }\r\n </div>\r\n</div>\r\n\r\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\r\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\r\n (onHide)=\"cancelReopenPivotDialog()\">\r\n <div class=\"route-popup-form\">\r\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\r\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\r\n appendTo=\"body\" />\r\n\r\n <div class=\"route-popup-actions\">\r\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\r\n }}</button>\r\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\r\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\r\n </div>\r\n </div>\r\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AutoCompleteModule }, { kind: "component", type: i2.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: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.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: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2$1.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: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "component", type: Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: TabList, selector: "p-tablist" }, { kind: "component", type: Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: TabPanels, selector: "p-tabpanels" }, { kind: "component", type: TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "ngmodule", type: MonacoEditorModule }, { kind: "component", type: i1$3.MonacoEditorComponent, selector: "ngx-monaco-editor", inputs: ["options", "uri"], outputs: ["init"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: DataSourceComponent, selector: "wuic-data-source", inputs: ["route", "routeFromRouting", "hardcodedRoute", "autoload", "loading", "changeTracking", "parentRecord", "parentMetaInfo", "parentDatasource", "useComboEndpoint", "comboFormulaLookup", "comboMcId", "comboField", "comboExtraFields", "comboCurrentRecord", "componentRef"] }, { kind: "component", type: FilterBarComponent, selector: "wuic-filter-bar", inputs: ["datasource", "hardcodedDatasource", "multiDatasourceMetas", "skipUrlSync", "skipFetch"], outputs: ["filterApplied", "onFilterApplied", "onSortingApplied", "onPageSizeApplied", "onGroupingChanged", "onFilterBarCollapseChanged"] }, { kind: "component", type: ViewBuilderComponent, selector: "wuic-view-builder", inputs: ["autoReflowLayout"], outputs: ["viewDefinitionChange"] }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
|
|
4940
4940
|
}
|
|
4941
4941
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, decorators: [{
|
|
4942
4942
|
type: Component,
|
|
4943
|
-
args: [{ selector: 'wuic-pivot-builder', imports: [NgClass, FormsModule, AutoCompleteModule, ButtonModule, SplitButtonModule, SelectModule, SplitterModule, AccordionModule, DialogModule, TableModule, Tabs, TabList, Tab, TabPanels, TabPanel, MonacoEditorModule, TranslateModule, DataSourceComponent, FilterBarComponent, ViewBuilderComponent], template: "<div class=\"pivot-builder\">\n <!-- ===== Global header (just title) ===== -->\n <div class=\"pivot-toolbar\">\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\n </div>\n\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\n </wuic-data-source>\n\n <!-- ===== Tabs ===== -->\n <p-tabs [(value)]=\"activeTab\">\n <p-tablist>\n <p-tab [value]=\"0\">\n <i class=\"pi pi-sitemap mr-2\"></i>\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\n </p-tab>\n <p-tab [value]=\"1\">\n <i class=\"pi pi-th-large mr-2\"></i>\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\n </p-tab>\n </p-tablist>\n <p-tabpanels>\n\n <!-- ==================== Tab 0: View Builder ==================== -->\n <p-tabpanel [value]=\"0\">\n <!-- View Builder toolbar -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar: tutti i button uniformati al tema selezionato\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\n Rimosse severity override per evitare mix `contrast/success/\n secondary` che rompeva la coerenza visiva.\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\n raggruppati nello split-button \"Definition\". -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\n </p-button>\n <p-splitButton #definitionSplitBtn\n [label]=\"'pivot_builder.vb.definition' | translate\"\n icon=\"pi pi-database\"\n [model]=\"definitionMenuItems\"\n styleClass=\"pivot-actions-definition-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\n </p-splitButton>\n </div>\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\n tab, pattern standard del framework \u2014 vedi list-grid.component\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\n span semplice (es. view creata senza scaffold, o payload\n legacy senza route). -->\n @if (createdViewName) {\n @if (createdViewRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + createdViewRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n </span>\n }\n }\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\n del menu e' un toggle con icona check/stop che riflette lo\n stato corrente (auto-generate, manual SQL, all-filterable,\n auto-reflow). Click sulla voce inverte il flag.\n Allineato a destra della toolbar tramite\n `.pivot-actions-settings` (margin-left: auto). -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #settingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"settingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\n <div class=\"pivot-filter-full\">\n <wuic-filter-bar\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\n [skipUrlSync]=\"true\"\n [skipFetch]=\"true\"\n (filterApplied)=\"onViewBuilderFilterApplied()\"\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\n </wuic-filter-bar>\n </div>\n }\n\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\n </wuic-view-builder>\n </div>\n </p-tabpanel>\n\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\n <p-tabpanel [value]=\"1\">\n <!-- Pivot toolbar: route selector + actions -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\n tab View Builder: nessuna severity override).\n \"Create View\", \"Create Materialized Table\",\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\n split-button \"Actions\" (stesso pattern del Definition\n split-button del tab View Builder). -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\n </p-button>\n <p-splitButton #pivotConfigSplitBtn\n [label]=\"'pivot_builder.actions' | translate\"\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\n ? 'pi pi-spin pi-spinner'\n : 'pi pi-ellipsis-v'\"\n [model]=\"pivotConfigMenuItems\"\n styleClass=\"pivot-actions-config-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\n </p-splitButton>\n <!-- Chip cliccabile della view/materialized-table scaffoldata\n dal pivot (pattern identico al chip createdViewName del\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\n Tooltip include il tipo (view o table) serializzato. -->\n @if (pivotCreatedName) {\n @if (pivotCreatedRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"(pivotCreatedKind === 'table'\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n </span>\n }\n }\n </div>\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\n del View Builder Settings (auto-generate, manual SQL,\n all-filterable) meno il `auto-reflow` che e' specifico del\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #pivotSettingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"pivotSettingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <table style=\"width: 100%;\">\n <tr>\n <td>\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\n styleClass=\"pivot-route-autocomplete\">\n </p-autoComplete>\n </td>\n <td>\n <label>{{ 'aggregation' | translate }}</label>\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"aggregateFunction\">\n </p-select>\n </td>\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\n il pageSize non e' stato applicato esplicitamente. -->\n </tr>\n </table>\n\n <!-- Filter panel -->\n <div class=\"pivot-filter-full\">\n\n @if (selectedRouteName) {\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\n (filterApplied)=\"onFilterBarApplied()\"\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\n }\n\n </div>\n\n <!-- 4-panel splitter: source cols | rows | columns | values -->\n <div class=\"pivot-buckets-row\">\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\n styleClass=\"pivot-buckets-splitter\">\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\n @for (col of sourceColumns; track col.alias) {\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\n <span>{{ col.label }}</span>\n <div class=\"pivot-column-actions\">\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\n }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\n }}</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\n @if (!rowColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\n }\n @for (col of rowColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\n @if (!columnColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\n }\n @for (col of columnColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\n @if (!valueColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\n }\n @for (col of valueColumns; track col.id) {\n <div class=\"pivot-chip\">\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\n <div class=\"pivot-chip-actions\">\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n </p-splitter>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n\n <!-- ===== Output row (below tabs, always visible) ===== -->\n <div class=\"pivot-output-row\">\n @if (!showQueryResults) {\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n } @else {\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\n <ng-template #panel>\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n </ng-template>\n <ng-template #panel>\n <div class=\"pivot-result-panel\">\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\n <ng-template pTemplate=\"header\">\n <tr>\n @for (c of queryResultColumns; track c) {\n <th>{{ c }}</th>\n }\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-row>\n <tr>\n @for (c of queryResultColumns; track c) {\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\n }\n </tr>\n </ng-template>\n </p-table>\n </div>\n </ng-template>\n </p-splitter>\n }\n </div>\n</div>\n\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenPivotDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [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)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"] }]
|
|
4943
|
+
args: [{ selector: 'wuic-pivot-builder', imports: [NgClass, FormsModule, AutoCompleteModule, ButtonModule, SplitButtonModule, SelectModule, SplitterModule, AccordionModule, DialogModule, TableModule, Tabs, TabList, Tab, TabPanels, TabPanel, MonacoEditorModule, TranslateModule, DataSourceComponent, FilterBarComponent, ViewBuilderComponent], template: "<div class=\"pivot-builder\">\r\n <!-- ===== Global header (just title) ===== -->\r\n <div class=\"pivot-toolbar\">\r\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\r\n </div>\r\n\r\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\r\n </wuic-data-source>\r\n\r\n <!-- ===== Tabs ===== -->\r\n <p-tabs [(value)]=\"activeTab\">\r\n <p-tablist>\r\n <p-tab [value]=\"0\">\r\n <i class=\"pi pi-sitemap mr-2\"></i>\r\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\r\n </p-tab>\r\n <p-tab [value]=\"1\">\r\n <i class=\"pi pi-th-large mr-2\"></i>\r\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\r\n </p-tab>\r\n </p-tablist>\r\n <p-tabpanels>\r\n\r\n <!-- ==================== Tab 0: View Builder ==================== -->\r\n <p-tabpanel [value]=\"0\">\r\n <!-- View Builder toolbar -->\r\n <div class=\"pivot-actions-row\">\r\n <!-- Toolbar: tutti i button uniformati al tema selezionato\r\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\r\n Rimosse severity override per evitare mix `contrast/success/\r\n secondary` che rompeva la coerenza visiva.\r\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\r\n raggruppati nello split-button \"Definition\". -->\r\n <div class=\"pivot-actions\">\r\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\r\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\r\n </p-button>\r\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\r\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\r\n </p-button>\r\n <p-splitButton #definitionSplitBtn\r\n [label]=\"'pivot_builder.vb.definition' | translate\"\r\n icon=\"pi pi-database\"\r\n [model]=\"definitionMenuItems\"\r\n styleClass=\"pivot-actions-definition-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\r\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\r\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\r\n tab, pattern standard del framework \u2014 vedi list-grid.component\r\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\r\n span semplice (es. view creata senza scaffold, o payload\r\n legacy senza route). -->\r\n @if (createdViewName) {\r\n @if (createdViewRoute) {\r\n <a class=\"pivot-created-view-label pivot-created-view-link\"\r\n [href]=\"'#/' + createdViewRoute + '/list'\"\r\n target=\"_blank\" rel=\"noopener\"\r\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\r\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\r\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\r\n </a>\r\n } @else {\r\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\r\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\r\n </span>\r\n }\r\n }\r\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\r\n del menu e' un toggle con icona check/stop che riflette lo\r\n stato corrente (auto-generate, manual SQL, all-filterable,\r\n auto-reflow). Click sulla voce inverte il flag.\r\n Allineato a destra della toolbar tramite\r\n `.pivot-actions-settings` (margin-left: auto). -->\r\n <div class=\"pivot-actions-settings\">\r\n <p-splitButton #settingsSplitBtn\r\n [label]=\"'pivot_builder.vb.settings' | translate\"\r\n icon=\"pi pi-cog\"\r\n [model]=\"settingsMenuItems\"\r\n styleClass=\"pivot-actions-settings-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\r\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n </div>\r\n\r\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\r\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\r\n <div class=\"pivot-filter-full\">\r\n <wuic-filter-bar\r\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\r\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\r\n [skipUrlSync]=\"true\"\r\n [skipFetch]=\"true\"\r\n (filterApplied)=\"onViewBuilderFilterApplied()\"\r\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\r\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\r\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\r\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\r\n </wuic-filter-bar>\r\n </div>\r\n }\r\n\r\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\r\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\r\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\r\n </wuic-view-builder>\r\n </div>\r\n </p-tabpanel>\r\n\r\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\r\n <p-tabpanel [value]=\"1\">\r\n <!-- Pivot toolbar: route selector + actions -->\r\n <div class=\"pivot-actions-row\">\r\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\r\n tab View Builder: nessuna severity override).\r\n \"Create View\", \"Create Materialized Table\",\r\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\r\n split-button \"Actions\" (stesso pattern del Definition\r\n split-button del tab View Builder). -->\r\n <div class=\"pivot-actions\">\r\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\r\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\r\n </p-button>\r\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\r\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\r\n </p-button>\r\n <p-splitButton #pivotConfigSplitBtn\r\n [label]=\"'pivot_builder.actions' | translate\"\r\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\r\n ? 'pi pi-spin pi-spinner'\r\n : 'pi pi-ellipsis-v'\"\r\n [model]=\"pivotConfigMenuItems\"\r\n styleClass=\"pivot-actions-config-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\r\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\r\n </p-splitButton>\r\n <!-- Chip cliccabile della view/materialized-table scaffoldata\r\n dal pivot (pattern identico al chip createdViewName del\r\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\r\n Tooltip include il tipo (view o table) serializzato. -->\r\n @if (pivotCreatedName) {\r\n @if (pivotCreatedRoute) {\r\n <a class=\"pivot-created-view-label pivot-created-view-link\"\r\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\r\n target=\"_blank\" rel=\"noopener\"\r\n [title]=\"(pivotCreatedKind === 'table'\r\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\r\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\r\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\r\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\r\n {{ pivotCreatedName }}\r\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\r\n </a>\r\n } @else {\r\n <span class=\"pivot-created-view-label\">\r\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\r\n {{ pivotCreatedName }}\r\n </span>\r\n }\r\n }\r\n </div>\r\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\r\n del View Builder Settings (auto-generate, manual SQL,\r\n all-filterable) meno il `auto-reflow` che e' specifico del\r\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\r\n <div class=\"pivot-actions-settings\">\r\n <p-splitButton #pivotSettingsSplitBtn\r\n [label]=\"'pivot_builder.vb.settings' | translate\"\r\n icon=\"pi pi-cog\"\r\n [model]=\"pivotSettingsMenuItems\"\r\n styleClass=\"pivot-actions-settings-split\"\r\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\r\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\r\n </p-splitButton>\r\n </div>\r\n </div>\r\n\r\n <table style=\"width: 100%;\">\r\n <tr>\r\n <td>\r\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\r\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\r\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\r\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\r\n styleClass=\"pivot-route-autocomplete\">\r\n </p-autoComplete>\r\n </td>\r\n <td>\r\n <label>{{ 'aggregation' | translate }}</label>\r\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [(ngModel)]=\"aggregateFunction\">\r\n </p-select>\r\n </td>\r\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\r\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\r\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\r\n il pageSize non e' stato applicato esplicitamente. -->\r\n </tr>\r\n </table>\r\n\r\n <!-- Filter panel -->\r\n <div class=\"pivot-filter-full\">\r\n\r\n @if (selectedRouteName) {\r\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\r\n (filterApplied)=\"onFilterBarApplied()\"\r\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\r\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\r\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\r\n }\r\n\r\n </div>\r\n\r\n <!-- 4-panel splitter: source cols | rows | columns | values -->\r\n <div class=\"pivot-buckets-row\">\r\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\r\n styleClass=\"pivot-buckets-splitter\">\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\r\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\r\n @for (col of sourceColumns; track col.alias) {\r\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\r\n <span>{{ col.label }}</span>\r\n <div class=\"pivot-column-actions\">\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\r\n }}</button>\r\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\r\n }}</button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\r\n @if (!rowColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\r\n }\r\n @for (col of rowColumns; track col.alias) {\r\n <div class=\"pivot-chip\">\r\n <span>{{ col.label }}</span>\r\n @if (isAxisDateCastAvailable(col)) {\r\n <label class=\"pivot-row-cast-flag\">\r\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\r\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\r\n {{ 'pivot_builder.cast_date' | translate }}\r\n </label>\r\n }\r\n @if (isAxisDateTimeGroupAvailable(col)) {\r\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n }\r\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\r\n @if (!columnColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\r\n }\r\n @for (col of columnColumns; track col.alias) {\r\n <div class=\"pivot-chip\">\r\n <span>{{ col.label }}</span>\r\n @if (isAxisDateCastAvailable(col)) {\r\n <label class=\"pivot-row-cast-flag\">\r\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\r\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\r\n {{ 'pivot_builder.cast_date' | translate }}\r\n </label>\r\n }\r\n @if (isAxisDateTimeGroupAvailable(col)) {\r\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n }\r\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #panel>\r\n <div class=\"pivot-panel\">\r\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\r\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\r\n @if (!valueColumns.length) {\r\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\r\n }\r\n @for (col of valueColumns; track col.id) {\r\n <div class=\"pivot-chip\">\r\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\r\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\r\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\r\n <div class=\"pivot-chip-actions\">\r\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\r\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\r\n </p-select>\r\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n </p-splitter>\r\n </div>\r\n </p-tabpanel>\r\n </p-tabpanels>\r\n </p-tabs>\r\n\r\n <!-- ===== Output row (below tabs, always visible) ===== -->\r\n <div class=\"pivot-output-row\">\r\n @if (!showQueryResults) {\r\n <div class=\"pivot-sql-editor\">\r\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\r\n @if (generatingViewDefSql || generatingSql) {\r\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\r\n }\r\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\r\n </div>\r\n } @else {\r\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\r\n <ng-template #panel>\r\n <div class=\"pivot-sql-editor\">\r\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\r\n @if (generatingViewDefSql || generatingSql) {\r\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\r\n }\r\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\r\n </div>\r\n </ng-template>\r\n <ng-template #panel>\r\n <div class=\"pivot-result-panel\">\r\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\r\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\r\n <ng-template pTemplate=\"header\">\r\n <tr>\r\n @for (c of queryResultColumns; track c) {\r\n <th>{{ c }}</th>\r\n }\r\n </tr>\r\n </ng-template>\r\n <ng-template pTemplate=\"body\" let-row>\r\n <tr>\r\n @for (c of queryResultColumns; track c) {\r\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\r\n }\r\n </tr>\r\n </ng-template>\r\n </p-table>\r\n </div>\r\n </ng-template>\r\n </p-splitter>\r\n }\r\n </div>\r\n</div>\r\n\r\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\r\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\r\n (onHide)=\"cancelReopenPivotDialog()\">\r\n <div class=\"route-popup-form\">\r\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\r\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\r\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\r\n appendTo=\"body\" />\r\n\r\n <div class=\"route-popup-actions\">\r\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\r\n }}</button>\r\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\r\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\r\n </div>\r\n </div>\r\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"] }]
|
|
4944
4944
|
}], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }, { type: ChatbotHostRegistryService }, { type: i0.ElementRef }], propDecorators: { nestedSource: [{
|
|
4945
4945
|
type: ViewChild,
|
|
4946
4946
|
args: ['nestedSource']
|
|
@@ -4950,4 +4950,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4950
4950
|
}] } });
|
|
4951
4951
|
|
|
4952
4952
|
export { PivotBuilderComponent };
|
|
4953
|
-
//# sourceMappingURL=wuic-framework-lib-pivot-builder.component-
|
|
4953
|
+
//# sourceMappingURL=wuic-framework-lib-pivot-builder.component-S5zqy3vj.mjs.map
|