wuic-framework-lib 1.3.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-DAP-TPJR.mjs → wuic-framework-lib-chart-list.component-Bl424n7x.mjs} +49 -37
- package/fesm2022/{wuic-framework-lib-code-editor.component-CLMDvOWs.mjs → wuic-framework-lib-code-editor.component-N6HMl2mf.mjs} +441 -277
- package/fesm2022/{wuic-framework-lib-designer.component-DA7abiLW.mjs → wuic-framework-lib-designer.component-Bd2fwl6H.mjs} +1948 -641
- package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BLrUrfZ9.mjs → wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs} +18 -20
- package/fesm2022/{wuic-framework-lib-framework-docs.component-DphKhTef.mjs → wuic-framework-lib-framework-docs.component-B867JAYe.mjs} +4205 -380
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-BqUUwrJM.mjs → wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs} +578 -66
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-DZrhwMH5.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs} +2 -2
- package/fesm2022/wuic-framework-lib-report-designer.component-QcrU81m6.mjs +380 -0
- package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs +431 -0
- package/fesm2022/{wuic-framework-lib-report-viewer.component-CIN2fY0P.mjs → wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs} +8 -13
- package/fesm2022/wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs +4813 -0
- package/fesm2022/wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs +303 -0
- package/fesm2022/wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs +2807 -0
- package/fesm2022/wuic-framework-lib-scene3d-viewer.component-Dboz1fjl.mjs +1130 -0
- package/fesm2022/{wuic-framework-lib-scheduler-list.component-CdcI1Uor.mjs → wuic-framework-lib-scheduler-list.component--wY21R3o.mjs} +169 -53
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-j1HTRI_P.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.mjs} +147 -106
- package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
- package/fesm2022/wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs +805 -0
- package/fesm2022/wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs +9504 -0
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-BRujEwpR.mjs → wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs} +14713 -8403
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +5 -2
- package/styles/_wuic-tokens.scss +82 -0
- package/styles/styles.scss +160 -0
- package/types/wuic-framework-lib.d.ts +5971 -3666
- package/fesm2022/wuic-framework-lib-report-designer.component-D_qbefud.mjs +0 -194
- package/fesm2022/wuic-framework-lib-workflow-designer.component-SE_7ZogW.mjs +0 -5114
|
@@ -13,7 +13,7 @@ import * as i4 from 'primeng/button';
|
|
|
13
13
|
import { ButtonModule } from 'primeng/button';
|
|
14
14
|
import * as i12 from 'primeng/splitbutton';
|
|
15
15
|
import { SplitButtonModule } from 'primeng/splitbutton';
|
|
16
|
-
import * as
|
|
16
|
+
import * as i5 from 'primeng/select';
|
|
17
17
|
import { SelectModule } from 'primeng/select';
|
|
18
18
|
import * as i10 from 'primeng/splitter';
|
|
19
19
|
import { SplitterModule } from 'primeng/splitter';
|
|
@@ -25,8 +25,8 @@ 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,
|
|
29
|
-
import * as
|
|
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-dfLTqam5.mjs';
|
|
29
|
+
import * as i7 from 'primeng/tooltip';
|
|
30
30
|
import { TooltipModule } from 'primeng/tooltip';
|
|
31
31
|
import { ClassicPreset, NodeEditor } from 'rete';
|
|
32
32
|
import { AreaExtensions, AreaPlugin } from 'rete-area-plugin';
|
|
@@ -239,7 +239,7 @@ class ViewBuilderNodeComponent {
|
|
|
239
239
|
</div>
|
|
240
240
|
</div>
|
|
241
241
|
</div>
|
|
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:
|
|
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,
|
|
@@ -506,8 +506,8 @@ class ViewBuilderLayoutHelper {
|
|
|
506
506
|
}
|
|
507
507
|
const node = ctx.editor.getNode(nodeId);
|
|
508
508
|
return {
|
|
509
|
-
w: Number(node?.width) || this.FALLBACK_W,
|
|
510
|
-
h: Number(node?.height) || this.FALLBACK_H,
|
|
509
|
+
w: Number((node)?.width) || this.FALLBACK_W,
|
|
510
|
+
h: Number((node)?.height) || this.FALLBACK_H,
|
|
511
511
|
};
|
|
512
512
|
}
|
|
513
513
|
/**
|
|
@@ -759,7 +759,7 @@ class ViewBuilderComponent {
|
|
|
759
759
|
let alias = String(this.newAggOutputAlias || '').trim();
|
|
760
760
|
if (!alias) {
|
|
761
761
|
// default: <fn>_<src> con underscores
|
|
762
|
-
alias = `${fn.toLowerCase()}_${src.
|
|
762
|
+
alias = `${fn.toLowerCase()}_${src.replaceAll(/[.*]/g, '_')}`;
|
|
763
763
|
}
|
|
764
764
|
this.aggregations.push({ sourceColumn: src, function: fn, outputAlias: alias });
|
|
765
765
|
// Reset form
|
|
@@ -1071,8 +1071,8 @@ class ViewBuilderComponent {
|
|
|
1071
1071
|
// L'utente puo' cambiare manualmente LEFT/RIGHT/FULL via context
|
|
1072
1072
|
// menu sulla connessione (vedi joinTypeOptions).
|
|
1073
1073
|
const conn = context.data;
|
|
1074
|
-
if (conn && !conn._joinType) {
|
|
1075
|
-
conn._joinType = this.connectionJoinTypes.get(conn.id) || 'INNER';
|
|
1074
|
+
if (conn && !(conn)._joinType) {
|
|
1075
|
+
(conn)._joinType = this.connectionJoinTypes.get(conn.id) || 'INNER';
|
|
1076
1076
|
}
|
|
1077
1077
|
this.refreshConnectedSockets();
|
|
1078
1078
|
this.emitViewDefinition();
|
|
@@ -1215,7 +1215,7 @@ class ViewBuilderComponent {
|
|
|
1215
1215
|
catch (err) {
|
|
1216
1216
|
WtoolboxService.messageNotificationService?.add({
|
|
1217
1217
|
severity: 'error', summary: 'Metadata Error',
|
|
1218
|
-
detail: `Cannot load metadata for "${option.value}": ${err?.message || err}`, life: 5000
|
|
1218
|
+
detail: `Cannot load metadata for "${option.value}": ${(err)?.message || err}`, life: 5000
|
|
1219
1219
|
});
|
|
1220
1220
|
return;
|
|
1221
1221
|
}
|
|
@@ -1272,20 +1272,9 @@ class ViewBuilderComponent {
|
|
|
1272
1272
|
}
|
|
1273
1273
|
};
|
|
1274
1274
|
// Callback for remove button
|
|
1275
|
-
node.onRemoveNode =
|
|
1276
|
-
if (!this.editor)
|
|
1277
|
-
return;
|
|
1278
|
-
// Remove all connections first
|
|
1279
|
-
for (const conn of this.editor.getConnections()) {
|
|
1280
|
-
if (conn.source === node.id || conn.target === node.id) {
|
|
1281
|
-
await this.editor.removeConnection(conn.id);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
await this.editor.removeNode(node.id);
|
|
1285
|
-
this.emitViewDefinition();
|
|
1286
|
-
};
|
|
1275
|
+
node.onRemoveNode = () => { void this.removeNodeWithConnections(node.id); };
|
|
1287
1276
|
// Callback for column formula (right-click)
|
|
1288
|
-
|
|
1277
|
+
const columnFormulaAsync = async (col) => {
|
|
1289
1278
|
const result = await WtoolboxService.promptDialog(`Column formula: ${col.label || col.alias}`, [
|
|
1290
1279
|
{ name: 'formula', caption: 'SQL Formula', value: col.formula || col.realName || col.alias, type: 'text' },
|
|
1291
1280
|
{ name: 'alias', caption: 'Output Alias', value: col.formulaAlias || col.realName || col.alias, type: 'text' }
|
|
@@ -1302,8 +1291,9 @@ class ViewBuilderComponent {
|
|
|
1302
1291
|
this.emitViewDefinition();
|
|
1303
1292
|
}
|
|
1304
1293
|
};
|
|
1294
|
+
node.onColumnFormula = (col) => { void columnFormulaAsync(col); };
|
|
1305
1295
|
// Callback rinomina alias tabella (right-click sull'header).
|
|
1306
|
-
node.onRenameAlias = () => this.promptRenameAlias(node);
|
|
1296
|
+
node.onRenameAlias = () => { void this.promptRenameAlias(node); };
|
|
1307
1297
|
// Add sockets: output on every column (drag from), input only on PK-like columns (drop onto)
|
|
1308
1298
|
for (const col of viewColumns) {
|
|
1309
1299
|
if (col.virtual)
|
|
@@ -1498,7 +1488,7 @@ class ViewBuilderComponent {
|
|
|
1498
1488
|
});
|
|
1499
1489
|
return;
|
|
1500
1490
|
}
|
|
1501
|
-
if (!/^[A-Za-z_]
|
|
1491
|
+
if (!/^[A-Za-z_]\w*$/.test(rawAliasInput)) {
|
|
1502
1492
|
WtoolboxService.messageNotificationService?.add?.({
|
|
1503
1493
|
severity: 'warn', summary: 'Alias invalido',
|
|
1504
1494
|
detail: 'Usa solo lettere, numeri e underscore; primo carattere non numerico.',
|
|
@@ -1555,7 +1545,7 @@ class ViewBuilderComponent {
|
|
|
1555
1545
|
// The connection containers are direct children of the area viewport.
|
|
1556
1546
|
// Match by checking all connections and their DOM positions.
|
|
1557
1547
|
const connections = this.editor.getConnections();
|
|
1558
|
-
|
|
1548
|
+
el.getBoundingClientRect();
|
|
1559
1549
|
// Simple heuristic: find the connection whose SVG container overlaps this element
|
|
1560
1550
|
for (const conn of connections) {
|
|
1561
1551
|
const connEl = document.querySelector(`[data-testid="connection-${conn.id}"]`);
|
|
@@ -1614,6 +1604,19 @@ class ViewBuilderComponent {
|
|
|
1614
1604
|
this.cdr.detectChanges();
|
|
1615
1605
|
}
|
|
1616
1606
|
emitDebounceTimer = null;
|
|
1607
|
+
// Callback condivisa dei pulsanti remove-node (S4144): rimuove prima le
|
|
1608
|
+
// connessioni del nodo, poi il nodo, e riemette la view definition.
|
|
1609
|
+
async removeNodeWithConnections(nodeId) {
|
|
1610
|
+
if (!this.editor)
|
|
1611
|
+
return;
|
|
1612
|
+
for (const conn of this.editor.getConnections()) {
|
|
1613
|
+
if (conn.source === nodeId || conn.target === nodeId) {
|
|
1614
|
+
await this.editor.removeConnection(conn.id);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
await this.editor.removeNode(nodeId);
|
|
1618
|
+
this.emitViewDefinition();
|
|
1619
|
+
}
|
|
1617
1620
|
emitViewDefinition() {
|
|
1618
1621
|
// Debounce: wait 500ms for all graph mutations to settle (e.g. rapid column toggles, auto-joins)
|
|
1619
1622
|
clearTimeout(this.emitDebounceTimer);
|
|
@@ -1819,18 +1822,8 @@ class ViewBuilderComponent {
|
|
|
1819
1822
|
}
|
|
1820
1823
|
}
|
|
1821
1824
|
};
|
|
1822
|
-
node.onRemoveNode =
|
|
1823
|
-
|
|
1824
|
-
return;
|
|
1825
|
-
for (const conn of this.editor.getConnections()) {
|
|
1826
|
-
if (conn.source === node.id || conn.target === node.id) {
|
|
1827
|
-
await this.editor.removeConnection(conn.id);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
await this.editor.removeNode(node.id);
|
|
1831
|
-
this.emitViewDefinition();
|
|
1832
|
-
};
|
|
1833
|
-
node.onColumnFormula = async (col) => {
|
|
1825
|
+
node.onRemoveNode = () => { void this.removeNodeWithConnections(node.id); };
|
|
1826
|
+
const columnFormulaAsync = async (col) => {
|
|
1834
1827
|
const result = await WtoolboxService.promptDialog(`Column formula: ${col.label || col.alias}`, [
|
|
1835
1828
|
{ name: 'formula', caption: 'SQL Formula', value: col.formula || col.realName || col.alias, type: 'text' },
|
|
1836
1829
|
{ name: 'alias', caption: 'Output Alias', value: col.formulaAlias || col.realName || col.alias, type: 'text' }
|
|
@@ -1846,7 +1839,8 @@ class ViewBuilderComponent {
|
|
|
1846
1839
|
this.emitViewDefinition();
|
|
1847
1840
|
}
|
|
1848
1841
|
};
|
|
1849
|
-
node.
|
|
1842
|
+
node.onColumnFormula = (col) => { void columnFormulaAsync(col); };
|
|
1843
|
+
node.onRenameAlias = () => { void this.promptRenameAlias(node); };
|
|
1850
1844
|
for (const col of t.columns) {
|
|
1851
1845
|
if (col.virtual)
|
|
1852
1846
|
continue;
|
|
@@ -1858,7 +1852,7 @@ class ViewBuilderComponent {
|
|
|
1858
1852
|
}
|
|
1859
1853
|
await this.editor.addNode(node);
|
|
1860
1854
|
await this.area.translate(node.id, { x: t.x, y: t.y });
|
|
1861
|
-
this.tableAliasCounter = Math.max(this.tableAliasCounter, parseInt(t.tableAlias.replace('t', '')) + 1);
|
|
1855
|
+
this.tableAliasCounter = Math.max(this.tableAliasCounter, Number.parseInt(t.tableAlias.replace('t', '')) + 1);
|
|
1862
1856
|
}
|
|
1863
1857
|
// Recreate connections
|
|
1864
1858
|
for (const j of def.joins) {
|
|
@@ -1912,7 +1906,7 @@ class ViewBuilderComponent {
|
|
|
1912
1906
|
const apiUrl = String(WtoolboxService.appSettings?.api_url || '').trim();
|
|
1913
1907
|
if (apiUrl)
|
|
1914
1908
|
return (apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`) + 'Meta/AsmxProxy/';
|
|
1915
|
-
return `${
|
|
1909
|
+
return `${globalThis.location.origin}/api/Meta/AsmxProxy/`;
|
|
1916
1910
|
}
|
|
1917
1911
|
async zoomToFit() {
|
|
1918
1912
|
if (this.area) {
|
|
@@ -1920,7 +1914,7 @@ class ViewBuilderComponent {
|
|
|
1920
1914
|
}
|
|
1921
1915
|
}
|
|
1922
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 });
|
|
1923
|
-
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: i6$1.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: i6.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\">\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" }] });
|
|
1924
1918
|
}
|
|
1925
1919
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, decorators: [{
|
|
1926
1920
|
type: Component,
|
|
@@ -1958,12 +1952,29 @@ function isMultiTableView(def) {
|
|
|
1958
1952
|
return (def?.tables?.length ?? 0) > 1;
|
|
1959
1953
|
}
|
|
1960
1954
|
|
|
1955
|
+
/** Kind delle proposte gestite da questa pagina. */
|
|
1956
|
+
const PIVOT_INJECT_KIND = 'pivot_inject';
|
|
1957
|
+
/** Operazioni che agiscono sul canvas del view-builder (tab 0). */
|
|
1958
|
+
const PIVOT_VIEW_ACTION_TYPES = new Set([
|
|
1959
|
+
'inject', 'add_table', 'remove_table', 'join', 'unjoin',
|
|
1960
|
+
'select_columns', 'add_aggregation', 'remove_aggregation',
|
|
1961
|
+
'set_group_by', 'set_column_formula',
|
|
1962
|
+
]);
|
|
1963
|
+
/** Funzioni di aggregazione ammesse dalla ViewDefinition. */
|
|
1964
|
+
const PIVOT_AGGREGATION_FUNCTIONS = ['SUM', 'COUNT', 'AVG', 'MIN', 'MAX', 'COUNT_DISTINCT'];
|
|
1965
|
+
/** Raggruppamenti temporali ammessi su un asse. */
|
|
1966
|
+
const PIVOT_DATE_GROUPS = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
|
1967
|
+
/** Limiti del dump di stato, per non saturare il budget del contesto. */
|
|
1968
|
+
const PIVOT_DUMP_MAX_TABLES = 8;
|
|
1969
|
+
const PIVOT_DUMP_MAX_COLS = 12;
|
|
1961
1970
|
class PivotBuilderComponent {
|
|
1962
1971
|
dataSrv;
|
|
1963
1972
|
metaSrv;
|
|
1964
1973
|
trslSrv;
|
|
1965
1974
|
aRoute;
|
|
1966
1975
|
licenseFeature;
|
|
1976
|
+
chatbotHostRegistry;
|
|
1977
|
+
hostElementRef;
|
|
1967
1978
|
activeTab = 0;
|
|
1968
1979
|
activeFilterPanel = 'filters';
|
|
1969
1980
|
pivotName = '';
|
|
@@ -2038,7 +2049,7 @@ class PivotBuilderComponent {
|
|
|
2038
2049
|
pivotCreatedKind = '';
|
|
2039
2050
|
/** True quando la pivot configuration corrente e' stata salvata (ha pivotName). */
|
|
2040
2051
|
get hasSavedPivot() {
|
|
2041
|
-
return !!(this.pivotName
|
|
2052
|
+
return !!(this.pivotName?.trim());
|
|
2042
2053
|
}
|
|
2043
2054
|
/** Flag loading per "Delete current pivot" del tab Pivot Config. */
|
|
2044
2055
|
deletingPivot = false;
|
|
@@ -2057,7 +2068,7 @@ class PivotBuilderComponent {
|
|
|
2057
2068
|
if (!this.hasSavedPivot)
|
|
2058
2069
|
return;
|
|
2059
2070
|
const name = this.pivotName;
|
|
2060
|
-
const hasLinkedObject = !!(this.pivotCreatedRoute
|
|
2071
|
+
const hasLinkedObject = !!(this.pivotCreatedRoute?.trim());
|
|
2061
2072
|
const warningMsg = this.t('pivot_builder.delete_pivot_message', 'The current pivot configuration will be permanently deleted. This action cannot be undone.') + ' "' + name + '"';
|
|
2062
2073
|
let alsoDropObject = false;
|
|
2063
2074
|
let linkedKindLabel = '';
|
|
@@ -2235,12 +2246,14 @@ class PivotBuilderComponent {
|
|
|
2235
2246
|
minimap: { enabled: false },
|
|
2236
2247
|
readOnly: true
|
|
2237
2248
|
};
|
|
2238
|
-
constructor(dataSrv, metaSrv, trslSrv, aRoute, licenseFeature) {
|
|
2249
|
+
constructor(dataSrv, metaSrv, trslSrv, aRoute, licenseFeature, chatbotHostRegistry, hostElementRef) {
|
|
2239
2250
|
this.dataSrv = dataSrv;
|
|
2240
2251
|
this.metaSrv = metaSrv;
|
|
2241
2252
|
this.trslSrv = trslSrv;
|
|
2242
2253
|
this.aRoute = aRoute;
|
|
2243
2254
|
this.licenseFeature = licenseFeature;
|
|
2255
|
+
this.chatbotHostRegistry = chatbotHostRegistry;
|
|
2256
|
+
this.hostElementRef = hostElementRef;
|
|
2244
2257
|
}
|
|
2245
2258
|
/**
|
|
2246
2259
|
* `true` quando il backend usa un dialect dove lo schema SQL Server-style
|
|
@@ -2718,7 +2731,7 @@ class PivotBuilderComponent {
|
|
|
2718
2731
|
// del pivot ("Dimensione pagina"). pageSize=0 (Illimitato) → server salta TOP/LIMIT.
|
|
2719
2732
|
// Hardcoded 0 prima → il preview non mostrava mai LIMIT anche dopo "Applica dimensione".
|
|
2720
2733
|
const dsPageSize = Number(ds?.pageSize ?? 0);
|
|
2721
|
-
const previewMaxRows = dsPageSize
|
|
2734
|
+
const previewMaxRows = Math.max(dsPageSize, 0);
|
|
2722
2735
|
const resp = await this.metaSrv.previewViewDefinition(this.viewDefinition, previewMaxRows, true, this.multiDatasourceFilterInfo ?? ds?.filterInfo ?? null);
|
|
2723
2736
|
if (!resp?.ok) {
|
|
2724
2737
|
throw new Error(resp?.error || this.t('pivot_builder.vb.error_generating_query', 'Error generating query.'));
|
|
@@ -2766,12 +2779,12 @@ class PivotBuilderComponent {
|
|
|
2766
2779
|
// Hardcoded 200 prima → ora rispettiamo il pageSize impostato dall'utente
|
|
2767
2780
|
// tramite la filter-bar del pivot (datasource.pageSize).
|
|
2768
2781
|
const dsPageSize = Number(ds?.pageSize ?? 0);
|
|
2769
|
-
const maxRows = dsPageSize
|
|
2782
|
+
const maxRows = Math.max(dsPageSize, 0);
|
|
2770
2783
|
const resp = await this.metaSrv.previewViewDefinition(this.viewDefinition || { tables: [], joins: [] }, maxRows, false, this.multiDatasourceFilterInfo ?? ds?.filterInfo ?? null, manualSql);
|
|
2771
2784
|
if (!resp?.ok) {
|
|
2772
2785
|
throw new Error(resp?.error || this.t('pivot_builder.vb.error_executing_query', 'Error executing query.'));
|
|
2773
2786
|
}
|
|
2774
|
-
this.queryResultColumns = Array.isArray(resp?.columns) ? resp.columns.map(
|
|
2787
|
+
this.queryResultColumns = Array.isArray(resp?.columns) ? resp.columns.map(String) : [];
|
|
2775
2788
|
this.queryResultRows = Array.isArray(resp?.rows) ? resp.rows : [];
|
|
2776
2789
|
// In manual mode, don't overwrite the editor content with the server response
|
|
2777
2790
|
if (!this.manualSqlMode) {
|
|
@@ -2799,13 +2812,6 @@ class PivotBuilderComponent {
|
|
|
2799
2812
|
this.rebuildPivotSettingsMenuItems();
|
|
2800
2813
|
this.rebuildDefinitionMenuItems();
|
|
2801
2814
|
this.rebuildPivotConfigMenuItems();
|
|
2802
|
-
// this.pivotDatasourceSub = this.pivotDatasourceComponentRef$.subscribe((ref) => {
|
|
2803
|
-
// if (ref?.component && this.pivotDatasource !== ref.component) {
|
|
2804
|
-
// this.pivotDatasource = ref.component;
|
|
2805
|
-
// this.pivotDatasourceRef$.next(ref.component);
|
|
2806
|
-
// this.applyPersistedGridStateToDatasource();
|
|
2807
|
-
// }
|
|
2808
|
-
// });
|
|
2809
2815
|
await this.loadRouteOptions();
|
|
2810
2816
|
const routeFromUrl = String(this.aRoute.snapshot?.paramMap?.get('route') || '').trim();
|
|
2811
2817
|
if (routeFromUrl) {
|
|
@@ -2826,6 +2832,19 @@ class PivotBuilderComponent {
|
|
|
2826
2832
|
this.pivotDatasourceRef$.next(this.nestedSource);
|
|
2827
2833
|
this.applyPersistedGridStateToDatasource();
|
|
2828
2834
|
}
|
|
2835
|
+
// Assistente contestuale. Registrato QUI e non in ngOnInit perche' il
|
|
2836
|
+
// @ViewChild viewBuilderRef non e' ancora risolto in ngOnInit.
|
|
2837
|
+
// Registra SOLO il pivot-builder, mai il view-builder figlio: il padre e'
|
|
2838
|
+
// owner di viewDefinition e della persistenza, e due state provider
|
|
2839
|
+
// registrati raddoppierebbero l'overhead nel budget di contesto.
|
|
2840
|
+
this.chatbotHostRegistry.register(PIVOT_INJECT_KIND, this.onChatbotProposedPivotAction.bind(this));
|
|
2841
|
+
this.chatbotHostRegistry.registerStateProvider(PIVOT_INJECT_KIND, this.buildCompactPivotStateDump.bind(this));
|
|
2842
|
+
try {
|
|
2843
|
+
const el = this.hostElementRef?.nativeElement;
|
|
2844
|
+
if (el)
|
|
2845
|
+
el.__wuicCmp = this;
|
|
2846
|
+
}
|
|
2847
|
+
catch { /* ambiente senza DOM */ }
|
|
2829
2848
|
}
|
|
2830
2849
|
async loadRouteOptions() {
|
|
2831
2850
|
try {
|
|
@@ -3221,9 +3240,10 @@ class PivotBuilderComponent {
|
|
|
3221
3240
|
.filter((x) => !!x.alias)
|
|
3222
3241
|
.map((x) => [x.alias, x.groupBy]));
|
|
3223
3242
|
const columnAliases = Array.isArray(config?.columnColumns) ? config.columnColumns : [];
|
|
3243
|
+
const singleValueAliases = String(config?.valueColumn || '').trim() ? [String(config.valueColumn).trim()] : [];
|
|
3224
3244
|
const valueAliases = Array.isArray(config?.valueColumns)
|
|
3225
3245
|
? config.valueColumns
|
|
3226
|
-
:
|
|
3246
|
+
: singleValueAliases;
|
|
3227
3247
|
const valueDefinitions = Array.isArray(config?.valueDefinitions) ? config.valueDefinitions : [];
|
|
3228
3248
|
const legacyValueAggregates = (config?.valueAggregates && typeof config.valueAggregates === 'object')
|
|
3229
3249
|
? config.valueAggregates
|
|
@@ -3490,7 +3510,7 @@ class PivotBuilderComponent {
|
|
|
3490
3510
|
if (!response?.ok) {
|
|
3491
3511
|
throw new Error(String(response?.error || this.t('pivot_builder.error.execute_query', 'Errore esecuzione query pivot.')));
|
|
3492
3512
|
}
|
|
3493
|
-
this.queryResultColumns = Array.isArray(response?.columns) ? response.columns.map(
|
|
3513
|
+
this.queryResultColumns = Array.isArray(response?.columns) ? response.columns.map(String) : [];
|
|
3494
3514
|
this.queryResultRows = Array.isArray(response?.rows) ? response.rows : [];
|
|
3495
3515
|
this.generatedSql = String(response?.executedSql || this.generatedSql || '');
|
|
3496
3516
|
this.showQueryResults = true;
|
|
@@ -4057,7 +4077,7 @@ class PivotBuilderComponent {
|
|
|
4057
4077
|
}
|
|
4058
4078
|
/** True quando la view definition corrente è stata salvata (ha un pivotName). */
|
|
4059
4079
|
get hasSavedDefinition() {
|
|
4060
|
-
return !!(this.pivotName
|
|
4080
|
+
return !!(this.pivotName?.trim());
|
|
4061
4081
|
}
|
|
4062
4082
|
deletingDefinition = false;
|
|
4063
4083
|
/**
|
|
@@ -4070,7 +4090,7 @@ class PivotBuilderComponent {
|
|
|
4070
4090
|
if (!this.hasSavedDefinition)
|
|
4071
4091
|
return;
|
|
4072
4092
|
const name = this.pivotName;
|
|
4073
|
-
const hasLinkedView = !!(this.createdViewRoute
|
|
4093
|
+
const hasLinkedView = !!(this.createdViewRoute?.trim());
|
|
4074
4094
|
const warningMsg = this.t('pivot_builder.vb.delete_definition_message', 'The current view definition will be permanently deleted. This action cannot be undone.') + ' "' + name + '"';
|
|
4075
4095
|
let alsoDropView = false;
|
|
4076
4096
|
if (hasLinkedView) {
|
|
@@ -4414,22 +4434,514 @@ class PivotBuilderComponent {
|
|
|
4414
4434
|
});
|
|
4415
4435
|
return;
|
|
4416
4436
|
}
|
|
4417
|
-
if (
|
|
4418
|
-
const base =
|
|
4437
|
+
if (globalThis.window !== undefined) {
|
|
4438
|
+
const base = globalThis.location.href.split('#')[0];
|
|
4419
4439
|
const url = `${base}#/${encodeURIComponent(pivotName)}/pivot-builder`;
|
|
4420
|
-
|
|
4440
|
+
globalThis.history.replaceState(null, '', url);
|
|
4421
4441
|
}
|
|
4422
4442
|
}
|
|
4423
4443
|
ngOnDestroy() {
|
|
4444
|
+
try {
|
|
4445
|
+
this.chatbotHostRegistry.unregister(PIVOT_INJECT_KIND);
|
|
4446
|
+
this.chatbotHostRegistry.unregisterStateProvider(PIVOT_INJECT_KIND);
|
|
4447
|
+
}
|
|
4448
|
+
catch { /* best-effort */ }
|
|
4449
|
+
try {
|
|
4450
|
+
const el = this.hostElementRef?.nativeElement;
|
|
4451
|
+
if (el)
|
|
4452
|
+
delete el.__wuicCmp;
|
|
4453
|
+
}
|
|
4454
|
+
catch { /* ambiente senza DOM */ }
|
|
4424
4455
|
this.pivotDatasourceSub?.unsubscribe();
|
|
4425
4456
|
}
|
|
4426
|
-
|
|
4427
|
-
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: i6$1.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" }] });
|
|
4457
|
+
// ==========================================================================
|
|
4458
|
+
// Assistente contestuale (kind `pivot_inject`)
|
|
4459
|
+
//
|
|
4460
|
+
// Le operazioni sul VIEW BUILDER passano dal ciclo
|
|
4461
|
+
// `serializeViewDefinition() -> muta la definizione -> restoreFromDefinition()`,
|
|
4462
|
+
// che e' l'unica API pubblica del figlio (editor e addTableNode sono privati)
|
|
4463
|
+
// ed e' gia' usata dal padre per ripristinare una configurazione salvata.
|
|
4464
|
+
// Eccezione: l'aggiunta di una tabella riusa `selectedRouteOption` +
|
|
4465
|
+
// `addSelectedRoute()`, cosi' si eredita il fetch dei metadata colonne,
|
|
4466
|
+
// l'auto-detect dei join da lookup e il layout automatico.
|
|
4467
|
+
//
|
|
4468
|
+
// Le operazioni sulla CONFIGURAZIONE PIVOT riusano i metodi pubblici
|
|
4469
|
+
// esistenti (addToRows/addToColumns/addToValues & co.), che de-duplicano tra
|
|
4470
|
+
// gli assi e riattivano l'auto-generate del SQL.
|
|
4471
|
+
// ==========================================================================
|
|
4472
|
+
/**
|
|
4473
|
+
* Applica una proposta dell'assistente. Pubblico: i test e2e deterministici
|
|
4474
|
+
* lo invocano via `__wuicCmp` senza coinvolgere il modello.
|
|
4475
|
+
*/
|
|
4476
|
+
async onChatbotProposedPivotAction(action) {
|
|
4477
|
+
try {
|
|
4478
|
+
const a = action;
|
|
4479
|
+
const actionType = String(a?.action_type ?? 'inject').trim();
|
|
4480
|
+
// Le operazioni sul canvas richiedono il tab del view-builder attivo: se
|
|
4481
|
+
// l'utente e' sul tab della configurazione, lo riportiamo sul canvas
|
|
4482
|
+
// (cosi' vede accadere la modifica invece di subirla in un tab nascosto).
|
|
4483
|
+
const isViewOp = PIVOT_VIEW_ACTION_TYPES.has(actionType);
|
|
4484
|
+
if (isViewOp) {
|
|
4485
|
+
const ready = await this.ensureViewBuilderReady();
|
|
4486
|
+
if (!ready) {
|
|
4487
|
+
return { ok: false, message: 'il View Builder non e\' disponibile: apri il tab "View Builder" e riprova' };
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
switch (actionType) {
|
|
4491
|
+
case 'inject': return await this.applyPivotInject(a);
|
|
4492
|
+
case 'add_table': return await this.applyPivotAddTable(String(a?.route ?? ''));
|
|
4493
|
+
case 'remove_table': return await this.applyPivotRemoveTable(a);
|
|
4494
|
+
case 'join': return await this.applyPivotJoin(a);
|
|
4495
|
+
case 'unjoin': return await this.applyPivotUnjoin(a);
|
|
4496
|
+
case 'select_columns': return this.applyPivotSelectColumns(a);
|
|
4497
|
+
case 'add_aggregation': return this.applyPivotAddAggregation(a);
|
|
4498
|
+
case 'remove_aggregation': return this.applyPivotRemoveAggregation(a);
|
|
4499
|
+
case 'set_group_by': return this.applyPivotSetGroupBy(a);
|
|
4500
|
+
case 'set_column_formula': return this.applyPivotSetColumnFormula(a);
|
|
4501
|
+
case 'pivot_set_axis': return this.applyPivotSetAxis(a);
|
|
4502
|
+
case 'pivot_remove': return this.applyPivotRemoveFromAxis(a);
|
|
4503
|
+
case 'pivot_set_value_options': return this.applyPivotValueOptions(a);
|
|
4504
|
+
case 'pivot_set_date_group': return this.applyPivotDateGroup(a);
|
|
4505
|
+
case 'pivot_set_top_rows': return this.applyPivotTopRows(a);
|
|
4506
|
+
default:
|
|
4507
|
+
return { ok: false, message: `operazione '${actionType}' non riconosciuta` };
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
catch (e) {
|
|
4511
|
+
return { ok: false, message: `errore nell'applicare la proposta: ${e?.message ?? e}` };
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
/** Porta in primo piano il tab del view-builder e attende che il ref sia risolto. */
|
|
4515
|
+
async ensureViewBuilderReady() {
|
|
4516
|
+
// Si assegna SEMPRE, senza chiedere prima `if (this.activeTab !== 0)`.
|
|
4517
|
+
// Motivo: dopo un cambio tab fatto dall'utente col mouse, `activeTab` puo'
|
|
4518
|
+
// restare indietro rispetto al tab realmente attivo (osservato: UI sul tab
|
|
4519
|
+
// "Pivot Config" mentre la proprieta' valeva ancora 0). Con la guardia
|
|
4520
|
+
// condizionale il componente si convinceva di essere gia' sul View Builder e
|
|
4521
|
+
// non lo portava in primo piano: l'operazione sul canvas avveniva in un tab
|
|
4522
|
+
// che l'utente non stava guardando. Riassegnare quando il valore e' gia'
|
|
4523
|
+
// corretto non ha effetti collaterali, quindi la via sicura e' assegnare
|
|
4524
|
+
// sempre e attendere un tick.
|
|
4525
|
+
this.activeTab = 0;
|
|
4526
|
+
// Un tick perche' il tab possa montare il canvas prima di usarlo.
|
|
4527
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4528
|
+
return !!this.viewBuilderRef;
|
|
4529
|
+
}
|
|
4530
|
+
/** Definizione corrente del canvas (vuota se il canvas non ha ancora nulla). */
|
|
4531
|
+
currentViewDefinition() {
|
|
4532
|
+
const def = this.viewBuilderRef?.serializeViewDefinition?.() ?? null;
|
|
4533
|
+
return def ?? { tables: [], joins: [] };
|
|
4534
|
+
}
|
|
4535
|
+
/** Riapplica una definizione al canvas e propaga lo stato al padre. */
|
|
4536
|
+
async pushViewDefinition(def) {
|
|
4537
|
+
if (!this.viewBuilderRef)
|
|
4538
|
+
throw new Error('view builder non disponibile');
|
|
4539
|
+
await this.viewBuilderRef.restoreFromDefinition(def);
|
|
4540
|
+
this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? def);
|
|
4541
|
+
}
|
|
4542
|
+
/** Risolve una tabella sul canvas per alias, route o caption (case-insensitive). */
|
|
4543
|
+
findViewTable(def, ref) {
|
|
4544
|
+
const needle = String(ref ?? '').trim().toLowerCase();
|
|
4545
|
+
if (!needle)
|
|
4546
|
+
return undefined;
|
|
4547
|
+
return def.tables.find((t) => t.tableAlias?.toLowerCase() === needle ||
|
|
4548
|
+
t.route?.toLowerCase() === needle ||
|
|
4549
|
+
t.caption?.toLowerCase() === needle);
|
|
4550
|
+
}
|
|
4551
|
+
/** Aggiunge una tabella al canvas riusando il flusso della palette. */
|
|
4552
|
+
async applyPivotAddTable(route) {
|
|
4553
|
+
const wanted = String(route ?? '').trim();
|
|
4554
|
+
if (!wanted)
|
|
4555
|
+
return { ok: false, message: 'route della tabella non indicata' };
|
|
4556
|
+
// NB: in RouteOption il nome della route sta in `value` (`label` e' la
|
|
4557
|
+
// caption mostrata nell'autocomplete).
|
|
4558
|
+
const option = this.routeOptions.find((o) => String(o.value ?? '').toLowerCase() === wanted.toLowerCase());
|
|
4559
|
+
if (!option) {
|
|
4560
|
+
return { ok: false, message: `route '${wanted}' non presente fra quelle disponibili. Vicine: ${this.suggestRoutes(wanted)}` };
|
|
4561
|
+
}
|
|
4562
|
+
if (this.viewBuilderRef?.isRouteOnCanvas?.(option.value)) {
|
|
4563
|
+
return { ok: true, message: `la tabella '${option.value}' e' gia' sul canvas.` };
|
|
4564
|
+
}
|
|
4565
|
+
this.viewBuilderRef.selectedRouteOption = option;
|
|
4566
|
+
await this.viewBuilderRef.addSelectedRoute();
|
|
4567
|
+
this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? null);
|
|
4568
|
+
return { ok: true, message: `tabella '${option.value}' aggiunta al canvas.` };
|
|
4569
|
+
}
|
|
4570
|
+
/** Le 3 route disponibili piu' simili, per rendere azionabile l'errore. */
|
|
4571
|
+
suggestRoutes(wanted) {
|
|
4572
|
+
const n = wanted.toLowerCase();
|
|
4573
|
+
const hits = this.routeOptions
|
|
4574
|
+
.map((o) => String(o.value ?? ''))
|
|
4575
|
+
.filter((r) => r && (r.toLowerCase().includes(n) || n.includes(r.toLowerCase())));
|
|
4576
|
+
return (hits.length > 0 ? hits : this.routeOptions.slice(0, 3).map((o) => String(o.value ?? '')))
|
|
4577
|
+
.slice(0, 3).join(', ') || '(nessuna)';
|
|
4578
|
+
}
|
|
4579
|
+
/** Rimuove una tabella dal canvas (e con essa i join che la toccano). */
|
|
4580
|
+
async applyPivotRemoveTable(a) {
|
|
4581
|
+
const def = this.currentViewDefinition();
|
|
4582
|
+
const table = this.findViewTable(def, a?.table_alias ?? a?.route);
|
|
4583
|
+
if (!table)
|
|
4584
|
+
return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non presente sul canvas` };
|
|
4585
|
+
await this.viewBuilderRef.removeNode(table.nodeId);
|
|
4586
|
+
this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? null);
|
|
4587
|
+
return { ok: true, message: `tabella '${table.caption || table.route}' rimossa.` };
|
|
4588
|
+
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Crea un join fra due tabelle del canvas.
|
|
4591
|
+
*
|
|
4592
|
+
* Le colonne vengono validate qui: la definizione viene riapplicata senza
|
|
4593
|
+
* passare dal pipe di validazione dei drop del canvas, quindi un riferimento
|
|
4594
|
+
* inesistente produrrebbe un join silenziosamente rotto.
|
|
4595
|
+
*/
|
|
4596
|
+
async applyPivotJoin(a) {
|
|
4597
|
+
const def = this.currentViewDefinition();
|
|
4598
|
+
const res = this.buildJoinEdge(def, a);
|
|
4599
|
+
if (typeof res === 'string')
|
|
4600
|
+
return { ok: false, message: res };
|
|
4601
|
+
def.joins = [...(def.joins ?? []), res];
|
|
4602
|
+
await this.pushViewDefinition(def);
|
|
4603
|
+
// Riletta la definizione: se il join non c'e', dirlo invece di riportare
|
|
4604
|
+
// un successo che l'utente non vedrebbe sul canvas.
|
|
4605
|
+
const after = this.currentViewDefinition();
|
|
4606
|
+
const created = (after.joins ?? []).some((j) => j.sourceColumn === res.sourceColumn && j.targetColumn === res.targetColumn &&
|
|
4607
|
+
j.sourceNodeId === res.sourceNodeId && j.targetNodeId === res.targetNodeId);
|
|
4608
|
+
if (!created)
|
|
4609
|
+
return { ok: false, message: 'il join non e\' stato creato dal canvas (verifica che le colonne siano compatibili)' };
|
|
4610
|
+
return { ok: true, message: `join ${res.sourceRoute}.${res.sourceColumn} -${res.joinType}-> ${res.targetRoute}.${res.targetColumn} creato.` };
|
|
4611
|
+
}
|
|
4612
|
+
/** Valida i riferimenti del join e costruisce l'arco, o ritorna il motivo del rifiuto. */
|
|
4613
|
+
buildJoinEdge(def, a) {
|
|
4614
|
+
const src = this.findViewTable(def, a?.source_alias ?? a?.source_route);
|
|
4615
|
+
const tgt = this.findViewTable(def, a?.target_alias ?? a?.target_route);
|
|
4616
|
+
if (!src)
|
|
4617
|
+
return `tabella sorgente '${a?.source_alias ?? a?.source_route}' non sul canvas`;
|
|
4618
|
+
if (!tgt)
|
|
4619
|
+
return `tabella destinazione '${a?.target_alias ?? a?.target_route}' non sul canvas`;
|
|
4620
|
+
if (src.nodeId === tgt.nodeId)
|
|
4621
|
+
return 'sorgente e destinazione sono la stessa tabella: self-join non ammesso';
|
|
4622
|
+
const srcCol = String(a?.source_column ?? '').trim();
|
|
4623
|
+
const tgtCol = String(a?.target_column ?? '').trim();
|
|
4624
|
+
const srcOk = src.columns?.some((c) => c.alias === srcCol || c.realName === srcCol);
|
|
4625
|
+
const tgtOk = tgt.columns?.some((c) => c.alias === tgtCol || c.realName === tgtCol);
|
|
4626
|
+
if (!srcOk)
|
|
4627
|
+
return `colonna '${srcCol}' inesistente su '${src.route}'. Disponibili: ${(src.columns ?? []).slice(0, 8).map((c) => c.alias).join(', ')}`;
|
|
4628
|
+
if (!tgtOk)
|
|
4629
|
+
return `colonna '${tgtCol}' inesistente su '${tgt.route}'. Disponibili: ${(tgt.columns ?? []).slice(0, 8).map((c) => c.alias).join(', ')}`;
|
|
4630
|
+
const jt = String(a?.join_type ?? 'INNER').trim().toUpperCase();
|
|
4631
|
+
const joinType = (['INNER', 'LEFT', 'RIGHT', 'FULL'].includes(jt) ? jt : 'INNER');
|
|
4632
|
+
return {
|
|
4633
|
+
edgeId: `chatbot-${src.nodeId}-${tgt.nodeId}-${srcCol}-${tgtCol}`,
|
|
4634
|
+
sourceNodeId: src.nodeId, sourceRoute: src.route, sourceColumn: srcCol,
|
|
4635
|
+
targetNodeId: tgt.nodeId, targetRoute: tgt.route, targetColumn: tgtCol,
|
|
4636
|
+
joinType, autoDetected: false,
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
/** Rimuove un join fra due tabelle. */
|
|
4640
|
+
async applyPivotUnjoin(a) {
|
|
4641
|
+
const def = this.currentViewDefinition();
|
|
4642
|
+
const src = this.findViewTable(def, a?.source_alias ?? a?.source_route);
|
|
4643
|
+
const tgt = this.findViewTable(def, a?.target_alias ?? a?.target_route);
|
|
4644
|
+
if (!src || !tgt)
|
|
4645
|
+
return { ok: false, message: 'tabelle del join non presenti sul canvas' };
|
|
4646
|
+
const before = (def.joins ?? []).length;
|
|
4647
|
+
def.joins = (def.joins ?? []).filter((j) => !(j.sourceNodeId === src.nodeId && j.targetNodeId === tgt.nodeId));
|
|
4648
|
+
if (def.joins.length === before)
|
|
4649
|
+
return { ok: false, message: 'nessun join fra le due tabelle indicate' };
|
|
4650
|
+
await this.pushViewDefinition(def);
|
|
4651
|
+
return { ok: true, message: `join ${src.route} -> ${tgt.route} rimosso.` };
|
|
4652
|
+
}
|
|
4653
|
+
/** Costruisce tabelle + join in una sola proposta (richieste composte). */
|
|
4654
|
+
async applyPivotInject(a) {
|
|
4655
|
+
const tables = Array.isArray(a?.tables) ? a.tables : [];
|
|
4656
|
+
const joins = Array.isArray(a?.joins) ? a.joins : [];
|
|
4657
|
+
if (tables.length === 0 && joins.length === 0) {
|
|
4658
|
+
return { ok: false, message: 'la proposta non contiene tabelle ne\' join' };
|
|
4659
|
+
}
|
|
4660
|
+
const added = [];
|
|
4661
|
+
const problems = [];
|
|
4662
|
+
for (const t of tables) {
|
|
4663
|
+
const r = await this.applyPivotAddTable(String(t?.route ?? ''));
|
|
4664
|
+
if (r.ok)
|
|
4665
|
+
added.push(String(t?.route ?? ''));
|
|
4666
|
+
else
|
|
4667
|
+
problems.push(r.message ?? 'errore');
|
|
4668
|
+
}
|
|
4669
|
+
let joinsCreated = 0;
|
|
4670
|
+
for (const j of joins) {
|
|
4671
|
+
const r = await this.applyPivotJoin(j);
|
|
4672
|
+
if (r.ok)
|
|
4673
|
+
joinsCreated++;
|
|
4674
|
+
else
|
|
4675
|
+
problems.push(r.message ?? 'errore');
|
|
4676
|
+
}
|
|
4677
|
+
const okAny = added.length > 0 || joinsCreated > 0;
|
|
4678
|
+
const summary = `${added.length} tabella(e) e ${joinsCreated} join applicati`
|
|
4679
|
+
+ (problems.length > 0 ? `. Non applicati: ${problems.join(' | ')}` : '.');
|
|
4680
|
+
return { ok: okAny, message: summary };
|
|
4681
|
+
}
|
|
4682
|
+
/** Attiva/disattiva colonne di una tabella nella vista. */
|
|
4683
|
+
applyPivotSelectColumns(a) {
|
|
4684
|
+
const def = this.currentViewDefinition();
|
|
4685
|
+
const table = this.findViewTable(def, a?.table_alias ?? a?.route);
|
|
4686
|
+
if (!table)
|
|
4687
|
+
return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non sul canvas` };
|
|
4688
|
+
const names = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
|
|
4689
|
+
if (names.length === 0)
|
|
4690
|
+
return { ok: false, message: 'nessuna colonna indicata' };
|
|
4691
|
+
const selected = a?.selected !== false;
|
|
4692
|
+
const missing = [];
|
|
4693
|
+
for (const n of names) {
|
|
4694
|
+
const col = table.columns?.find((c) => c.alias === n || c.realName === n || c.label === n);
|
|
4695
|
+
if (!col) {
|
|
4696
|
+
missing.push(n);
|
|
4697
|
+
continue;
|
|
4698
|
+
}
|
|
4699
|
+
col.selected = selected;
|
|
4700
|
+
}
|
|
4701
|
+
if (missing.length === names.length) {
|
|
4702
|
+
return { ok: false, message: `colonne inesistenti su '${table.route}': ${missing.join(', ')}` };
|
|
4703
|
+
}
|
|
4704
|
+
void this.pushViewDefinition(def);
|
|
4705
|
+
const suffix = missing.length > 0 ? ` (ignorate: ${missing.join(', ')})` : '';
|
|
4706
|
+
return { ok: true, message: `${names.length - missing.length} colonna(e) ${selected ? 'selezionate' : 'deselezionate'}${suffix}.` };
|
|
4707
|
+
}
|
|
4708
|
+
/** Aggiunge un'aggregazione alla definizione della vista. */
|
|
4709
|
+
applyPivotAddAggregation(a) {
|
|
4710
|
+
const src = String(a?.source_column ?? '').trim();
|
|
4711
|
+
const fn = String(a?.function ?? '').trim().toUpperCase();
|
|
4712
|
+
if (!src)
|
|
4713
|
+
return { ok: false, message: 'source_column mancante' };
|
|
4714
|
+
if (!PIVOT_AGGREGATION_FUNCTIONS.includes(fn)) {
|
|
4715
|
+
return { ok: false, message: `funzione '${fn}' non ammessa (${PIVOT_AGGREGATION_FUNCTIONS.join('|')})` };
|
|
4716
|
+
}
|
|
4717
|
+
const def = this.currentViewDefinition();
|
|
4718
|
+
const alias = String(a?.output_alias ?? '').trim() || `${fn.toLowerCase()}_${src.replace(/[^A-Za-z0-9_]/g, '_')}`;
|
|
4719
|
+
def.aggregations = [...(def.aggregations ?? []), {
|
|
4720
|
+
sourceColumn: src,
|
|
4721
|
+
function: fn,
|
|
4722
|
+
outputAlias: alias,
|
|
4723
|
+
caption: a?.caption ? String(a.caption) : undefined,
|
|
4724
|
+
}];
|
|
4725
|
+
void this.pushViewDefinition(def);
|
|
4726
|
+
return { ok: true, message: `aggregazione ${fn}(${src}) AS ${alias} aggiunta.` };
|
|
4727
|
+
}
|
|
4728
|
+
/** Rimuove un'aggregazione per alias di output. */
|
|
4729
|
+
applyPivotRemoveAggregation(a) {
|
|
4730
|
+
const alias = String(a?.output_alias ?? '').trim();
|
|
4731
|
+
if (!alias)
|
|
4732
|
+
return { ok: false, message: 'output_alias mancante' };
|
|
4733
|
+
const def = this.currentViewDefinition();
|
|
4734
|
+
const before = (def.aggregations ?? []).length;
|
|
4735
|
+
def.aggregations = (def.aggregations ?? []).filter((x) => x.outputAlias !== alias);
|
|
4736
|
+
if ((def.aggregations ?? []).length === before)
|
|
4737
|
+
return { ok: false, message: `nessuna aggregazione con alias '${alias}'` };
|
|
4738
|
+
void this.pushViewDefinition(def);
|
|
4739
|
+
return { ok: true, message: `aggregazione '${alias}' rimossa.` };
|
|
4740
|
+
}
|
|
4741
|
+
/** Sostituisce l'elenco delle colonne di GROUP BY. */
|
|
4742
|
+
applyPivotSetGroupBy(a) {
|
|
4743
|
+
const cols = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
|
|
4744
|
+
const def = this.currentViewDefinition();
|
|
4745
|
+
def.groupByColumns = cols;
|
|
4746
|
+
void this.pushViewDefinition(def);
|
|
4747
|
+
return { ok: true, message: cols.length > 0 ? `GROUP BY impostato su: ${cols.join(', ')}.` : 'GROUP BY azzerato.' };
|
|
4748
|
+
}
|
|
4749
|
+
/** Imposta una formula SQL su una colonna della vista. */
|
|
4750
|
+
applyPivotSetColumnFormula(a) {
|
|
4751
|
+
const def = this.currentViewDefinition();
|
|
4752
|
+
const table = this.findViewTable(def, a?.table_alias ?? a?.route);
|
|
4753
|
+
if (!table)
|
|
4754
|
+
return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non sul canvas` };
|
|
4755
|
+
const name = String(a?.column ?? '').trim();
|
|
4756
|
+
const col = table.columns?.find((c) => c.alias === name || c.realName === name || c.label === name);
|
|
4757
|
+
if (!col)
|
|
4758
|
+
return { ok: false, message: `colonna '${name}' inesistente su '${table.route}'` };
|
|
4759
|
+
col.formula = String(a?.formula ?? '') || undefined;
|
|
4760
|
+
if (a?.formula_alias)
|
|
4761
|
+
col.formulaAlias = String(a.formula_alias);
|
|
4762
|
+
void this.pushViewDefinition(def);
|
|
4763
|
+
return { ok: true, message: `formula impostata su ${table.route}.${name}.` };
|
|
4764
|
+
}
|
|
4765
|
+
/** Popola un asse del pivot con le colonne indicate. */
|
|
4766
|
+
applyPivotSetAxis(a) {
|
|
4767
|
+
const target = String(a?.target ?? '').trim().toLowerCase();
|
|
4768
|
+
if (!['rows', 'columns', 'values'].includes(target)) {
|
|
4769
|
+
return { ok: false, message: `target '${target}' non valido (rows|columns|values)` };
|
|
4770
|
+
}
|
|
4771
|
+
const names = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
|
|
4772
|
+
if (names.length === 0)
|
|
4773
|
+
return { ok: false, message: 'nessuna colonna indicata' };
|
|
4774
|
+
const applied = [];
|
|
4775
|
+
const missing = [];
|
|
4776
|
+
for (const n of names) {
|
|
4777
|
+
const col = this.findSourceColumn(n);
|
|
4778
|
+
if (!col) {
|
|
4779
|
+
missing.push(n);
|
|
4780
|
+
continue;
|
|
4781
|
+
}
|
|
4782
|
+
if (target === 'rows')
|
|
4783
|
+
this.addToRows(col);
|
|
4784
|
+
else if (target === 'columns')
|
|
4785
|
+
this.addToColumns(col);
|
|
4786
|
+
else
|
|
4787
|
+
this.addToValues(col);
|
|
4788
|
+
applied.push(col.alias);
|
|
4789
|
+
}
|
|
4790
|
+
if (applied.length === 0) {
|
|
4791
|
+
return { ok: false, message: `colonne non disponibili: ${missing.join(', ')}. Disponibili: ${this.sourceColumns.slice(0, 10).map((c) => c.alias).join(', ')}` };
|
|
4792
|
+
}
|
|
4793
|
+
// Per i valori, aggregazione/caption opzionali si applicano a tutti quelli appena aggiunti.
|
|
4794
|
+
if (target === 'values' && (a?.aggregate || a?.caption)) {
|
|
4795
|
+
for (const v of this.valueColumns.filter((v) => applied.includes(v.alias))) {
|
|
4796
|
+
const id = v.id ?? v.alias;
|
|
4797
|
+
if (a?.aggregate)
|
|
4798
|
+
this.setValueAggregate(id, String(a.aggregate));
|
|
4799
|
+
if (a?.caption)
|
|
4800
|
+
this.setValueCaption(id, String(a.caption));
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
const suffix = missing.length > 0 ? ` (ignorate: ${missing.join(', ')})` : '';
|
|
4804
|
+
return { ok: true, message: `${applied.join(', ')} aggiunta(e) a ${target}${suffix}.` };
|
|
4805
|
+
}
|
|
4806
|
+
/** Colonna disponibile per alias/label (case-insensitive). */
|
|
4807
|
+
findSourceColumn(name) {
|
|
4808
|
+
const n = String(name ?? '').trim().toLowerCase();
|
|
4809
|
+
return this.sourceColumns.find((c) => c.alias?.toLowerCase() === n || c.label?.toLowerCase() === n || c.realName?.toLowerCase() === n);
|
|
4810
|
+
}
|
|
4811
|
+
/** Toglie una colonna da un asse. */
|
|
4812
|
+
applyPivotRemoveFromAxis(a) {
|
|
4813
|
+
const target = String(a?.target ?? '').trim().toLowerCase();
|
|
4814
|
+
const col = String(a?.column ?? '').trim();
|
|
4815
|
+
if (!col)
|
|
4816
|
+
return { ok: false, message: 'column mancante' };
|
|
4817
|
+
switch (target) {
|
|
4818
|
+
case 'rows':
|
|
4819
|
+
this.removeFromRows(col);
|
|
4820
|
+
break;
|
|
4821
|
+
case 'columns':
|
|
4822
|
+
this.removeFromColumns(col);
|
|
4823
|
+
break;
|
|
4824
|
+
case 'values': {
|
|
4825
|
+
const v = this.valueColumns.find((x) => x.alias === col || x.id === col);
|
|
4826
|
+
this.removeFromValues((v?.id ?? col));
|
|
4827
|
+
break;
|
|
4828
|
+
}
|
|
4829
|
+
default: return { ok: false, message: `target '${target}' non valido (rows|columns|values)` };
|
|
4830
|
+
}
|
|
4831
|
+
return { ok: true, message: `'${col}' rimossa da ${target}.` };
|
|
4832
|
+
}
|
|
4833
|
+
/** Aggregazione/caption di una colonna in valori. */
|
|
4834
|
+
applyPivotValueOptions(a) {
|
|
4835
|
+
const col = String(a?.column ?? '').trim();
|
|
4836
|
+
const v = this.valueColumns.find((x) => x.alias === col || x.id === col);
|
|
4837
|
+
if (!v)
|
|
4838
|
+
return { ok: false, message: `'${col}' non e' fra le colonne dei valori` };
|
|
4839
|
+
const id = (v.id ?? v.alias);
|
|
4840
|
+
if (a?.aggregate)
|
|
4841
|
+
this.setValueAggregate(id, String(a.aggregate));
|
|
4842
|
+
if (a?.caption)
|
|
4843
|
+
this.setValueCaption(id, String(a.caption));
|
|
4844
|
+
return { ok: true, message: `opzioni aggiornate per '${col}'.` };
|
|
4845
|
+
}
|
|
4846
|
+
/** Raggruppamento temporale su un asse. */
|
|
4847
|
+
applyPivotDateGroup(a) {
|
|
4848
|
+
const target = String(a?.target ?? '').trim().toLowerCase();
|
|
4849
|
+
const col = String(a?.column ?? '').trim();
|
|
4850
|
+
const groupBy = String(a?.group_by ?? '').trim().toLowerCase();
|
|
4851
|
+
if (!col)
|
|
4852
|
+
return { ok: false, message: 'column mancante' };
|
|
4853
|
+
if (!PIVOT_DATE_GROUPS.includes(groupBy)) {
|
|
4854
|
+
return { ok: false, message: `group_by '${groupBy}' non valido (${PIVOT_DATE_GROUPS.join('|')})` };
|
|
4855
|
+
}
|
|
4856
|
+
if (target === 'rows')
|
|
4857
|
+
this.setRowDateGroupBy(col, groupBy);
|
|
4858
|
+
else if (target === 'columns')
|
|
4859
|
+
this.setColumnDateGroupBy(col, groupBy);
|
|
4860
|
+
else
|
|
4861
|
+
return { ok: false, message: `target '${target}' non valido (rows|columns)` };
|
|
4862
|
+
return { ok: true, message: `'${col}' raggruppata per ${groupBy}.` };
|
|
4863
|
+
}
|
|
4864
|
+
/** Limite di righe della preview. */
|
|
4865
|
+
applyPivotTopRows(a) {
|
|
4866
|
+
const n = Number(a?.top_rows);
|
|
4867
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
4868
|
+
return { ok: false, message: 'top_rows deve essere un numero positivo' };
|
|
4869
|
+
this.topRows = this.normalizeTopRows(n);
|
|
4870
|
+
return { ok: true, message: `limite righe impostato a ${this.topRows}.` };
|
|
4871
|
+
}
|
|
4872
|
+
/**
|
|
4873
|
+
* Snapshot compatto di vista + configurazione pivot per il contesto del
|
|
4874
|
+
* modello. Ritorna `null` a pagina vuota, per non spendere token inutilmente.
|
|
4875
|
+
*
|
|
4876
|
+
* La whitelist delle route sta in TESTA e non in coda: il contesto viene
|
|
4877
|
+
* troncato a lunghezza fissa lato server, e una whitelist tagliata a metà
|
|
4878
|
+
* porta il modello a inventare nomi di tabella.
|
|
4879
|
+
*/
|
|
4880
|
+
buildCompactPivotStateDump() {
|
|
4881
|
+
const def = this.viewBuilderRef?.serializeViewDefinition?.() ?? this.viewDefinition;
|
|
4882
|
+
const hasAxes = this.rowColumns.length > 0 || this.columnColumns.length > 0 || this.valueColumns.length > 0;
|
|
4883
|
+
const hasTables = !!def?.tables?.length;
|
|
4884
|
+
// NB: NON si ritorna null a pagina vuota. Il dump e' cio' che dice al modello
|
|
4885
|
+
// "sei sul pivot-builder": porta la whitelist delle route e — tramite l'header
|
|
4886
|
+
// del registry — la nota di disambiguazione. Senza, su una pagina appena
|
|
4887
|
+
// aperta il modello non ha modo di sapere dove si trova e non propone
|
|
4888
|
+
// `pivot_inject` (misurato: nessuna proposta, quindi nessun box "Applica").
|
|
4889
|
+
// Si ritorna null solo se non c'e' NIENTE da dire, whitelist inclusa.
|
|
4890
|
+
if (!hasTables && !hasAxes && !this.selectedRouteName && this.routeOptions.length === 0)
|
|
4891
|
+
return null;
|
|
4892
|
+
const lines = [];
|
|
4893
|
+
const routes = this.routeOptions.map((o) => String(o.value ?? '')).filter(Boolean);
|
|
4894
|
+
lines.push(routes.length > 0
|
|
4895
|
+
? `ROUTE METADATA DISPONIBILI (${routes.length}): ${routes.slice(0, 60).join(', ')}${routes.length > 60 ? ', …' : ''}`
|
|
4896
|
+
: 'ROUTE METADATA DISPONIBILI: <caricamento in corso>');
|
|
4897
|
+
lines.push(`TAB ATTIVO: ${this.activeTab === 0 ? '0 (View Builder)' : '1 (Pivot Config)'}`);
|
|
4898
|
+
if (hasTables) {
|
|
4899
|
+
lines.push(`VIEW BUILDER — TABELLE (${def.tables.length}):`);
|
|
4900
|
+
for (const t of def.tables.slice(0, PIVOT_DUMP_MAX_TABLES)) {
|
|
4901
|
+
const cols = (t.columns ?? []).filter((c) => c.selected);
|
|
4902
|
+
const shown = cols.slice(0, PIVOT_DUMP_MAX_COLS).map((c) => c.alias).join(',');
|
|
4903
|
+
const more = cols.length > PIVOT_DUMP_MAX_COLS ? ` (+${cols.length - PIVOT_DUMP_MAX_COLS})` : '';
|
|
4904
|
+
lines.push(`- ${t.tableAlias} ${t.caption || t.route} route=${t.route} sel=${cols.length}/${(t.columns ?? []).length}: ${shown}${more}`);
|
|
4905
|
+
}
|
|
4906
|
+
if (def.tables.length > PIVOT_DUMP_MAX_TABLES)
|
|
4907
|
+
lines.push(`- … (+${def.tables.length - PIVOT_DUMP_MAX_TABLES} tabelle)`);
|
|
4908
|
+
const joins = def.joins ?? [];
|
|
4909
|
+
if (joins.length > 0) {
|
|
4910
|
+
lines.push(`JOIN (${joins.length}):`);
|
|
4911
|
+
for (const j of joins.slice(0, PIVOT_DUMP_MAX_TABLES)) {
|
|
4912
|
+
lines.push(`- ${j.sourceRoute}.${j.sourceColumn} -${j.joinType}-> ${j.targetRoute}.${j.targetColumn}`);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
const aggs = def.aggregations ?? [];
|
|
4916
|
+
if (aggs.length > 0) {
|
|
4917
|
+
lines.push('AGGREGAZIONI: ' + aggs.map((x) => `${x.function}(${x.sourceColumn}) AS ${x.outputAlias}`).join('; '));
|
|
4918
|
+
}
|
|
4919
|
+
if (def.groupByColumns?.length)
|
|
4920
|
+
lines.push('GROUP BY: ' + def.groupByColumns.join(', '));
|
|
4921
|
+
}
|
|
4922
|
+
lines.push(`PIVOT CONFIG — route=${this.selectedRouteName || 'n/d'} topRows=${this.topRows}`);
|
|
4923
|
+
lines.push(`- ROWS: ${this.describeAxis(this.rowColumns)}`);
|
|
4924
|
+
lines.push(`- COLUMNS: ${this.describeAxis(this.columnColumns)}`);
|
|
4925
|
+
lines.push(`- VALUES: ${this.valueColumns.map((v) => `${v.alias}(${v.aggregate ?? 'SUM'})`).join(', ') || '(vuoto)'}`);
|
|
4926
|
+
if (this.sourceColumns.length > 0) {
|
|
4927
|
+
const names = this.sourceColumns.slice(0, PIVOT_DUMP_MAX_COLS * 2).map((c) => c.alias);
|
|
4928
|
+
lines.push(`COLONNE DISPONIBILI (${this.sourceColumns.length}): ${names.join(', ')}${this.sourceColumns.length > names.length ? ', …' : ''}`);
|
|
4929
|
+
}
|
|
4930
|
+
return lines.join('\n');
|
|
4931
|
+
}
|
|
4932
|
+
/** Descrizione compatta di un asse (con eventuale raggruppamento temporale). */
|
|
4933
|
+
describeAxis(axis) {
|
|
4934
|
+
if (!axis || axis.length === 0)
|
|
4935
|
+
return '(vuoto)';
|
|
4936
|
+
return axis.map((c) => c.dateGroupBy ? `${c.alias}(groupBy=${c.dateGroupBy})` : c.alias).join(', ');
|
|
4937
|
+
}
|
|
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" }] });
|
|
4428
4940
|
}
|
|
4429
4941
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, decorators: [{
|
|
4430
4942
|
type: Component,
|
|
4431
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"] }]
|
|
4432
|
-
}], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }], propDecorators: { nestedSource: [{
|
|
4944
|
+
}], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }, { type: ChatbotHostRegistryService }, { type: i0.ElementRef }], propDecorators: { nestedSource: [{
|
|
4433
4945
|
type: ViewChild,
|
|
4434
4946
|
args: ['nestedSource']
|
|
4435
4947
|
}], viewBuilderRef: [{
|
|
@@ -4438,4 +4950,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4438
4950
|
}] } });
|
|
4439
4951
|
|
|
4440
4952
|
export { PivotBuilderComponent };
|
|
4441
|
-
//# sourceMappingURL=wuic-framework-lib-pivot-builder.component-
|
|
4953
|
+
//# sourceMappingURL=wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs.map
|