@zentto/report-designer 1.5.6 → 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;
@@ -2320,10 +2328,13 @@ REGLAS:
2320
2328
  @click=${() => this._activePanel = 'toolbox'}>Toolbox</div>
2321
2329
  <div class="panel-tab ${this._activePanel === 'data' ? 'active' : ''}"
2322
2330
  @click=${() => this._activePanel = 'data'}>Data</div>
2331
+ <div class="panel-tab ${this._activePanel === 'er' ? 'active' : ''}"
2332
+ @click=${() => this._activePanel = 'er'}>ER</div>
2323
2333
  </div>
2324
2334
  <div class="panel-content">
2325
2335
  ${this._activePanel === 'toolbox' ? this._renderToolbox() : null}
2326
2336
  ${this._activePanel === 'data' ? this._renderDataPanel() : null}
2337
+ ${this._activePanel === 'er' ? this._renderErPanel() : null}
2327
2338
  </div>
2328
2339
  </div>
2329
2340
 
@@ -2961,28 +2972,100 @@ REGLAS:
2961
2972
  `)}
2962
2973
  `;
2963
2974
  }
2964
- // ─── Data Panel ───────────────────────────────────────────────
2975
+ // ─── Data Panel (Tree) ────────────────────────────────────────
2965
2976
  _renderDataPanel() {
2966
- if (this._layout.dataSources.length === 0) {
2967
- return html `<div style="color:var(--zrd-text-muted);padding:12px;">No data sources configured.</div>`;
2968
- }
2969
2977
  return html `
2970
- ${this._layout.dataSources.map(ds => html `
2971
- <div class="ds-name">${ds.name} (${ds.type})</div>
2972
- ${(ds.fields || []).map(f => html `
2973
- <div class="ds-field" draggable="true"
2974
- @dragstart=${(e) => {
2975
- e.dataTransfer?.setData('element-type', 'field');
2976
- e.dataTransfer?.setData('field-ds', ds.id);
2977
- e.dataTransfer?.setData('field-name', f.name);
2978
- }}>
2979
- <span class="ds-field-icon">F</span>
2980
- ${f.label || f.name} <span style="color:var(--zrd-text-muted)">(${f.type})</span>
2981
- </div>
2982
- `)}
2983
- `)}
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>
2992
+ `;
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>
2984
3008
  `;
2985
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
+ }
2986
3069
  // ─── Band Rendering ───────────────────────────────────────────
2987
3070
  _renderBand(band) {
2988
3071
  const bandHeightPx = this._toPx(band.height);
@@ -3004,7 +3087,7 @@ REGLAS:
3004
3087
  }}>
3005
3088
  <div class="band-header-left">
3006
3089
  <span class="band-type-icon">${BAND_ICONS[band.type] || ''}</span>
3007
- <span>${BAND_LABELS[band.type] || band.type}</span>
3090
+ <span>${this._getBandLabel(band)}</span>
3008
3091
  </div>
3009
3092
  <span style="font-weight:normal">${band.height}${this._unit}</span>
3010
3093
  </div>
@@ -3012,12 +3095,13 @@ REGLAS:
3012
3095
  style="height:${bandHeightPx}px;background-color:${bgColor};background-size:${gridSizePx}px ${gridSizePx}px;"
3013
3096
  @drop=${(e) => this._onBandDrop(e, band.id)}
3014
3097
  @dragover=${this._onBandDragOver}
3015
- @click=${(e) => {
3016
- e.stopPropagation();
3017
- this._selectedBandId = band.id;
3018
- this._selectedElementIds = new Set();
3019
- /* properties panel is always visible on right */
3020
- 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
+ }
3021
3105
  }}>
3022
3106
  ${band.elements.length === 0 ? html `<div class="band-empty-hint">Drag elements from Toolbox or drop fields from Data panel</div>` : nothing}
3023
3107
  ${band.elements.map(el => this._renderDesignElement(el, band.id))}
@@ -3032,6 +3116,56 @@ REGLAS:
3032
3116
  </div>
3033
3117
  `;
3034
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
+ }
3035
3169
  // ─── Element Rendering (Canvas) ───────────────────────────────
3036
3170
  _renderDesignElement(el, bandId) {
3037
3171
  const isSingle = this._selectedElementIds.size === 1 && this._selectedElementIds.has(el.id);
@@ -3073,15 +3207,10 @@ REGLAS:
3073
3207
  break;
3074
3208
  default: content = `[${el.type}]`;
3075
3209
  }
3076
- const elStyle = `
3077
- left:${xPx}px;top:${yPx}px;width:${wPx}px;height:${hPx}px;
3078
- font-size:${style.fontSize || 10}px;
3079
- font-weight:${style.fontWeight || 'normal'};
3080
- font-family:${style.fontFamily || 'inherit'};
3081
- color:${style.color || 'inherit'};
3082
- background:${style.backgroundColor || 'transparent'};
3083
- text-align:${style.textAlign || 'left'};
3084
- `;
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}`;
3085
3214
  if (el.type === 'line') {
3086
3215
  return html `
3087
3216
  <div class="element ${isSingle ? 'selected' : ''} ${isMulti ? 'multi-selected' : ''}"
@@ -3778,6 +3907,9 @@ __decorate([
3778
3907
  __decorate([
3779
3908
  property({ type: Array })
3780
3909
  ], ZenttoReportDesigner.prototype, "dataSources", void 0);
3910
+ __decorate([
3911
+ property({ attribute: false })
3912
+ ], ZenttoReportDesigner.prototype, "dataProvider", void 0);
3781
3913
  __decorate([
3782
3914
  property({ type: Number, attribute: 'grid-snap' })
3783
3915
  ], ZenttoReportDesigner.prototype, "gridSnap", void 0);
@@ -3826,6 +3958,12 @@ __decorate([
3826
3958
  __decorate([
3827
3959
  state()
3828
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);
3829
3967
  __decorate([
3830
3968
  state()
3831
3969
  ], ZenttoReportDesigner.prototype, "_undoStack", void 0);