@zentto/report-designer 1.5.5 → 1.5.7

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.
@@ -7,7 +7,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  // @zentto/report-designer — WYSIWYG report designer web component
8
8
  import { LitElement, html, css, nothing } from 'lit';
9
9
  import { customElement, property, state } from 'lit/decorators.js';
10
- import { createBlankLayout, toPx, PAGE_SIZES } from '@zentto/report-core';
10
+ import { createBlankLayout, toPx, PAGE_SIZES, mergeStyles, styleToCss } from '@zentto/report-core';
11
+ // Data panel components (tree, relation editor, ER diagram)
12
+ import './data-panel/data-source-tree.js';
13
+ import './data-panel/relation-editor.js';
14
+ import './data-panel/er-diagram.js';
11
15
  const BAND_LABELS = {
12
16
  reportHeader: 'Report Header',
13
17
  pageHeader: 'Page Header',
@@ -83,6 +87,8 @@ let ZenttoReportDesigner = class ZenttoReportDesigner extends LitElement {
83
87
  this.sampleData = null;
84
88
  /** Available data sources for the field picker */
85
89
  this.dataSources = [];
90
+ /** Data panel provider for lazy-loading from DB (Studio integration) */
91
+ this.dataProvider = null;
86
92
  /** Grid snap size in layout units (mm) */
87
93
  this.gridSnap = 1;
88
94
  /** Show live preview panel */
@@ -109,6 +115,8 @@ let ZenttoReportDesigner = class ZenttoReportDesigner extends LitElement {
109
115
  this._resize = null;
110
116
  this._bandResize = null;
111
117
  this._activePanel = 'toolbox';
118
+ this._relationEditorOpen = false;
119
+ this._editingRelation = null;
112
120
  this._undoStack = [];
113
121
  this._redoStack = [];
114
122
  this._editingReportName = false;
@@ -1988,6 +1996,27 @@ let ZenttoReportDesigner = class ZenttoReportDesigner extends LitElement {
1988
1996
  this._pasteClipboard();
1989
1997
  }
1990
1998
  }
1999
+ // Arrow keys to move selected elements (hold Ctrl for 0.5mm precision)
2000
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && this._selectedElementIds.size > 0 && this._selectedBandId) {
2001
+ e.preventDefault();
2002
+ const step = e.ctrlKey ? 0.5 : (this.gridSnap || 1);
2003
+ const band = this._layout.bands.find(b => b.id === this._selectedBandId);
2004
+ if (band) {
2005
+ for (const el of band.elements) {
2006
+ if (!this._selectedElementIds.has(el.id))
2007
+ continue;
2008
+ if (e.key === 'ArrowLeft')
2009
+ el.x = Math.max(0, el.x - step);
2010
+ if (e.key === 'ArrowRight')
2011
+ el.x += step;
2012
+ if (e.key === 'ArrowUp')
2013
+ el.y = Math.max(0, el.y - step);
2014
+ if (e.key === 'ArrowDown')
2015
+ el.y += step;
2016
+ }
2017
+ this._commitChange();
2018
+ }
2019
+ }
1991
2020
  // Escape to deselect or close context menu
1992
2021
  if (e.key === 'Escape') {
1993
2022
  if (this._contextMenu) {
@@ -2299,10 +2328,13 @@ REGLAS:
2299
2328
  @click=${() => this._activePanel = 'toolbox'}>Toolbox</div>
2300
2329
  <div class="panel-tab ${this._activePanel === 'data' ? 'active' : ''}"
2301
2330
  @click=${() => this._activePanel = 'data'}>Data</div>
2331
+ <div class="panel-tab ${this._activePanel === 'er' ? 'active' : ''}"
2332
+ @click=${() => this._activePanel = 'er'}>ER</div>
2302
2333
  </div>
2303
2334
  <div class="panel-content">
2304
2335
  ${this._activePanel === 'toolbox' ? this._renderToolbox() : null}
2305
2336
  ${this._activePanel === 'data' ? this._renderDataPanel() : null}
2337
+ ${this._activePanel === 'er' ? this._renderErPanel() : null}
2306
2338
  </div>
2307
2339
  </div>
2308
2340
 
@@ -2940,28 +2972,100 @@ REGLAS:
2940
2972
  `)}
2941
2973
  `;
2942
2974
  }
2943
- // ─── Data Panel ───────────────────────────────────────────────
2975
+ // ─── Data Panel (Tree) ────────────────────────────────────────
2944
2976
  _renderDataPanel() {
2945
- if (this._layout.dataSources.length === 0) {
2946
- return html `<div style="color:var(--zrd-text-muted);padding:12px;">No data sources configured.</div>`;
2947
- }
2948
2977
  return html `
2949
- ${this._layout.dataSources.map(ds => html `
2950
- <div class="ds-name">${ds.name} (${ds.type})</div>
2951
- ${(ds.fields || []).map(f => html `
2952
- <div class="ds-field" draggable="true"
2953
- @dragstart=${(e) => {
2954
- e.dataTransfer?.setData('element-type', 'field');
2955
- e.dataTransfer?.setData('field-ds', ds.id);
2956
- e.dataTransfer?.setData('field-name', f.name);
2957
- }}>
2958
- <span class="ds-field-icon">F</span>
2959
- ${f.label || f.name} <span style="color:var(--zrd-text-muted)">(${f.type})</span>
2960
- </div>
2961
- `)}
2962
- `)}
2978
+ <zrd-data-source-tree
2979
+ .dataSources=${this._layout.dataSources}
2980
+ .relations=${this._layout.relations || []}
2981
+ .dataProvider=${this.dataProvider}
2982
+ @tree-action=${(e) => this._onTreeAction(e.detail)}
2983
+ @field-double-click=${(e) => this._onFieldDoubleClick(e.detail)}
2984
+ ></zrd-data-source-tree>
2985
+ <zrd-relation-editor
2986
+ .dataSources=${this._layout.dataSources}
2987
+ .relation=${this._editingRelation}
2988
+ .open=${this._relationEditorOpen}
2989
+ @relation-save=${(e) => this._onRelationSave(e.detail.relation)}
2990
+ @relation-cancel=${() => { this._relationEditorOpen = false; this._editingRelation = null; }}
2991
+ ></zrd-relation-editor>
2963
2992
  `;
2964
2993
  }
2994
+ // ─── ER Diagram Panel ──────────────────────────────────────────
2995
+ _renderErPanel() {
2996
+ return html `
2997
+ <zrd-er-diagram
2998
+ .dataSources=${this._layout.dataSources}
2999
+ .relations=${this._layout.relations || []}
3000
+ @relation-edit=${(e) => {
3001
+ this._editingRelation = e.detail.relation;
3002
+ this._relationEditorOpen = true;
3003
+ }}
3004
+ @table-focus=${(e) => {
3005
+ this._activePanel = 'data';
3006
+ }}
3007
+ ></zrd-er-diagram>
3008
+ `;
3009
+ }
3010
+ // ─── Tree/Relation Event Handlers ──────────────────────────────
3011
+ _onTreeAction(detail) {
3012
+ switch (detail.action) {
3013
+ case 'create-relation':
3014
+ this._editingRelation = null;
3015
+ this._relationEditorOpen = true;
3016
+ break;
3017
+ case 'show-in-er':
3018
+ this._activePanel = 'er';
3019
+ break;
3020
+ case 'add-all-fields': {
3021
+ const dsId = detail.node.dataSourceId;
3022
+ if (!dsId)
3023
+ return;
3024
+ const ds = this._layout.dataSources.find(d => d.id === dsId);
3025
+ if (!ds?.fields)
3026
+ return;
3027
+ // Find or create detail band
3028
+ let detailBand = this._layout.bands.find(b => b.type === 'detail');
3029
+ if (!detailBand)
3030
+ return;
3031
+ let xOffset = 2;
3032
+ for (const f of ds.fields) {
3033
+ this._addElementToBand(detailBand.id, 'field', {
3034
+ dataSource: dsId,
3035
+ field: f.name,
3036
+ x: xOffset,
3037
+ y: 2,
3038
+ });
3039
+ xOffset += 35;
3040
+ }
3041
+ break;
3042
+ }
3043
+ }
3044
+ }
3045
+ _onFieldDoubleClick(detail) {
3046
+ // Add field to detail band at next available position
3047
+ const detailBand = this._layout.bands.find(b => b.type === 'detail');
3048
+ if (!detailBand)
3049
+ return;
3050
+ this._addElementToBand(detailBand.id, 'field', {
3051
+ dataSource: detail.dataSourceId,
3052
+ field: detail.field.name,
3053
+ });
3054
+ }
3055
+ _onRelationSave(relation) {
3056
+ const relations = [...(this._layout.relations || [])];
3057
+ const idx = relations.findIndex(r => r.id === relation.id);
3058
+ if (idx >= 0) {
3059
+ relations[idx] = relation;
3060
+ }
3061
+ else {
3062
+ relations.push(relation);
3063
+ }
3064
+ this._layout = { ...this._layout, relations };
3065
+ this._relationEditorOpen = false;
3066
+ this._editingRelation = null;
3067
+ this._commitChange();
3068
+ }
2965
3069
  // ─── Band Rendering ───────────────────────────────────────────
2966
3070
  _renderBand(band) {
2967
3071
  const bandHeightPx = this._toPx(band.height);
@@ -2983,7 +3087,7 @@ REGLAS:
2983
3087
  }}>
2984
3088
  <div class="band-header-left">
2985
3089
  <span class="band-type-icon">${BAND_ICONS[band.type] || ''}</span>
2986
- <span>${BAND_LABELS[band.type] || band.type}</span>
3090
+ <span>${this._getBandLabel(band)}</span>
2987
3091
  </div>
2988
3092
  <span style="font-weight:normal">${band.height}${this._unit}</span>
2989
3093
  </div>
@@ -2991,12 +3095,13 @@ REGLAS:
2991
3095
  style="height:${bandHeightPx}px;background-color:${bgColor};background-size:${gridSizePx}px ${gridSizePx}px;"
2992
3096
  @drop=${(e) => this._onBandDrop(e, band.id)}
2993
3097
  @dragover=${this._onBandDragOver}
2994
- @click=${(e) => {
2995
- e.stopPropagation();
2996
- this._selectedBandId = band.id;
2997
- this._selectedElementIds = new Set();
2998
- /* properties panel is always visible on right */
2999
- this.requestUpdate();
3098
+ @mousedown=${(e) => {
3099
+ // Only deselect if clicking directly on the band body (not on a child element)
3100
+ if (e.target === e.currentTarget) {
3101
+ this._selectedBandId = band.id;
3102
+ this._selectedElementIds = new Set();
3103
+ this.requestUpdate();
3104
+ }
3000
3105
  }}>
3001
3106
  ${band.elements.length === 0 ? html `<div class="band-empty-hint">Drag elements from Toolbox or drop fields from Data panel</div>` : nothing}
3002
3107
  ${band.elements.map(el => this._renderDesignElement(el, band.id))}
@@ -3011,6 +3116,56 @@ REGLAS:
3011
3116
  </div>
3012
3117
  `;
3013
3118
  }
3119
+ // ─── Sub-Band Helpers ──────────────────────────────────────────
3120
+ /** Generate dynamic band label with sub-band suffix (A, B, C…) */
3121
+ _getBandLabel(band) {
3122
+ const base = BAND_LABELS[band.type] || band.type;
3123
+ // Count how many bands of same type exist
3124
+ const siblingsCount = this._layout.bands.filter(b => b.type === band.type && b.visible !== false).length;
3125
+ if (siblingsCount <= 1 && (band.subBandIndex ?? 0) === 0)
3126
+ return base;
3127
+ const suffix = String.fromCharCode(65 + (band.subBandIndex ?? 0)); // A, B, C…
3128
+ return `${base} ${suffix}`;
3129
+ }
3130
+ /** Add a sub-band of the same type below the selected band */
3131
+ _addSubBand(bandId) {
3132
+ const band = this._layout.bands.find(b => b.id === bandId);
3133
+ if (!band)
3134
+ return;
3135
+ const siblings = this._layout.bands.filter(b => b.type === band.type);
3136
+ const maxIdx = Math.max(...siblings.map(b => b.subBandIndex ?? 0));
3137
+ const newBand = {
3138
+ id: `band_${Date.now()}`,
3139
+ type: band.type,
3140
+ height: band.height,
3141
+ elements: [],
3142
+ subBandIndex: maxIdx + 1,
3143
+ dataSource: band.dataSource,
3144
+ groupField: band.groupField,
3145
+ };
3146
+ // Insert after the current band
3147
+ const bands = [...this._layout.bands];
3148
+ const idx = bands.findIndex(b => b.id === bandId);
3149
+ bands.splice(idx + 1, 0, newBand);
3150
+ this._layout = { ...this._layout, bands };
3151
+ this._commitChange();
3152
+ }
3153
+ /** Remove a sub-band (only if there are multiple of same type) */
3154
+ _removeSubBand(bandId) {
3155
+ const band = this._layout.bands.find(b => b.id === bandId);
3156
+ if (!band)
3157
+ return;
3158
+ const siblings = this._layout.bands.filter(b => b.type === band.type);
3159
+ if (siblings.length <= 1)
3160
+ return; // Don't remove the last one
3161
+ this._layout = {
3162
+ ...this._layout,
3163
+ bands: this._layout.bands.filter(b => b.id !== bandId),
3164
+ };
3165
+ if (this._selectedBandId === bandId)
3166
+ this._selectedBandId = null;
3167
+ this._commitChange();
3168
+ }
3014
3169
  // ─── Element Rendering (Canvas) ───────────────────────────────
3015
3170
  _renderDesignElement(el, bandId) {
3016
3171
  const isSingle = this._selectedElementIds.size === 1 && this._selectedElementIds.has(el.id);
@@ -3052,15 +3207,10 @@ REGLAS:
3052
3207
  break;
3053
3208
  default: content = `[${el.type}]`;
3054
3209
  }
3055
- const elStyle = `
3056
- left:${xPx}px;top:${yPx}px;width:${wPx}px;height:${hPx}px;
3057
- font-size:${style.fontSize || 10}px;
3058
- font-weight:${style.fontWeight || 'normal'};
3059
- font-family:${style.fontFamily || 'inherit'};
3060
- color:${style.color || 'inherit'};
3061
- background:${style.backgroundColor || 'transparent'};
3062
- text-align:${style.textAlign || 'left'};
3063
- `;
3210
+ // Merge default + element style (same as viewer/renderer for pixel-perfect)
3211
+ const merged = mergeStyles(this._layout.defaultStyle, style);
3212
+ const cssStyle = styleToCss(merged);
3213
+ const elStyle = `left:${xPx}px;top:${yPx}px;width:${wPx}px;height:${hPx}px;${cssStyle}`;
3064
3214
  if (el.type === 'line') {
3065
3215
  return html `
3066
3216
  <div class="element ${isSingle ? 'selected' : ''} ${isMulti ? 'multi-selected' : ''}"
@@ -3128,32 +3278,68 @@ REGLAS:
3128
3278
  if (!this._contextMenu)
3129
3279
  return nothing;
3130
3280
  const { x, y, elementId, bandId } = this._contextMenu;
3281
+ const el = this._layout.bands.find(b => b.id === bandId)?.elements.find(e => e.id === elementId);
3282
+ const elType = el?.type || '';
3131
3283
  return html `
3132
3284
  <div class="overlay-dismiss" @click=${() => { this._contextMenu = null; this.requestUpdate(); }}></div>
3133
3285
  <div class="context-menu" style="left:${x}px;top:${y}px;">
3286
+ ${el ? html `
3287
+ <div class="context-menu-item" style="font-weight:600;color:var(--zrd-accent);pointer-events:none;font-size:11px;">
3288
+ ${elType.toUpperCase()} — ${el.id}
3289
+ </div>
3290
+ <div class="context-menu-sep"></div>
3291
+ ` : nothing}
3134
3292
  <div class="context-menu-item" @click=${() => { this._copySelected(); this._contextMenu = null; this.requestUpdate(); }}>
3135
- Copy <span class="context-menu-shortcut">Ctrl+C</span>
3293
+ Copiar <span class="context-menu-shortcut">Ctrl+C</span>
3136
3294
  </div>
3137
3295
  <div class="context-menu-item" @click=${() => { this._pasteClipboard(); this._contextMenu = null; this.requestUpdate(); }}>
3138
- Paste <span class="context-menu-shortcut">Ctrl+V</span>
3296
+ Pegar <span class="context-menu-shortcut">Ctrl+V</span>
3297
+ </div>
3298
+ <div class="context-menu-item" @click=${() => {
3299
+ if (el && bandId) {
3300
+ const band = this._layout.bands.find(b => b.id === bandId);
3301
+ if (band) {
3302
+ const clone = JSON.parse(JSON.stringify(el));
3303
+ clone.id = `${el.id}_dup_${this._idCounter++}`;
3304
+ clone.x += 2;
3305
+ clone.y += 2;
3306
+ band.elements.push(clone);
3307
+ this._selectedElementIds = new Set([clone.id]);
3308
+ this._commitChange();
3309
+ }
3310
+ }
3311
+ this._contextMenu = null;
3312
+ }}>
3313
+ Duplicar <span class="context-menu-shortcut">Alt+Drag</span>
3139
3314
  </div>
3140
3315
  <div class="context-menu-sep"></div>
3141
3316
  <div class="context-menu-item" @click=${() => { this._changeZOrder(elementId, bandId, 'front'); this._contextMenu = null; }}>
3142
- Bring to Front
3317
+ Traer al frente
3143
3318
  </div>
3144
3319
  <div class="context-menu-item" @click=${() => { this._changeZOrder(elementId, bandId, 'forward'); this._contextMenu = null; }}>
3145
- Bring Forward
3320
+ Adelante
3146
3321
  </div>
3147
3322
  <div class="context-menu-item" @click=${() => { this._changeZOrder(elementId, bandId, 'backward'); this._contextMenu = null; }}>
3148
- Send Backward
3323
+ Atras
3149
3324
  </div>
3150
3325
  <div class="context-menu-item" @click=${() => { this._changeZOrder(elementId, bandId, 'back'); this._contextMenu = null; }}>
3151
- Send to Back
3326
+ Enviar al fondo
3152
3327
  </div>
3328
+ ${el ? html `
3329
+ <div class="context-menu-sep"></div>
3330
+ <div class="context-menu-item" @click=${() => {
3331
+ this._selectedElementIds = new Set([elementId]);
3332
+ this._selectedBandId = bandId;
3333
+ this._contextMenu = null;
3334
+ this.requestUpdate();
3335
+ }}>
3336
+ Propiedades...
3337
+ </div>
3338
+ ` : nothing}
3153
3339
  <div class="context-menu-sep"></div>
3154
3340
  <div class="context-menu-item" style="color:var(--zrd-danger);"
3155
3341
  @click=${() => { this._deleteSelectedElements(); this._contextMenu = null; }}>
3156
- Delete <span class="context-menu-shortcut">Del</span>
3342
+ Eliminar <span class="context-menu-shortcut">Del</span>
3157
3343
  </div>
3158
3344
  </div>
3159
3345
  `;
@@ -3721,6 +3907,9 @@ __decorate([
3721
3907
  __decorate([
3722
3908
  property({ type: Array })
3723
3909
  ], ZenttoReportDesigner.prototype, "dataSources", void 0);
3910
+ __decorate([
3911
+ property({ attribute: false })
3912
+ ], ZenttoReportDesigner.prototype, "dataProvider", void 0);
3724
3913
  __decorate([
3725
3914
  property({ type: Number, attribute: 'grid-snap' })
3726
3915
  ], ZenttoReportDesigner.prototype, "gridSnap", void 0);
@@ -3769,6 +3958,12 @@ __decorate([
3769
3958
  __decorate([
3770
3959
  state()
3771
3960
  ], ZenttoReportDesigner.prototype, "_activePanel", void 0);
3961
+ __decorate([
3962
+ state()
3963
+ ], ZenttoReportDesigner.prototype, "_relationEditorOpen", void 0);
3964
+ __decorate([
3965
+ state()
3966
+ ], ZenttoReportDesigner.prototype, "_editingRelation", void 0);
3772
3967
  __decorate([
3773
3968
  state()
3774
3969
  ], ZenttoReportDesigner.prototype, "_undoStack", void 0);