@zentto/report-designer 1.5.6 → 1.6.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.
@@ -7,7 +7,12 @@ 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, DB connector)
12
+ import './data-panel/data-source-tree.js';
13
+ import './data-panel/relation-editor.js';
14
+ import './data-panel/er-diagram.js';
15
+ import './data-panel/db-connector.js';
11
16
  const BAND_LABELS = {
12
17
  reportHeader: 'Report Header',
13
18
  pageHeader: 'Page Header',
@@ -83,6 +88,10 @@ let ZenttoReportDesigner = class ZenttoReportDesigner extends LitElement {
83
88
  this.sampleData = null;
84
89
  /** Available data sources for the field picker */
85
90
  this.dataSources = [];
91
+ /** Data panel provider for lazy-loading from DB (Studio integration) */
92
+ this.dataProvider = null;
93
+ /** DB connector provider — enables native database connections in the web component */
94
+ this.dbProvider = null;
86
95
  /** Grid snap size in layout units (mm) */
87
96
  this.gridSnap = 1;
88
97
  /** Show live preview panel */
@@ -109,6 +118,10 @@ let ZenttoReportDesigner = class ZenttoReportDesigner extends LitElement {
109
118
  this._resize = null;
110
119
  this._bandResize = null;
111
120
  this._activePanel = 'toolbox';
121
+ this._relationEditorOpen = false;
122
+ this._editingRelation = null;
123
+ this._erOverlayOpen = false;
124
+ this._dbConnectorOpen = false;
112
125
  this._undoStack = [];
113
126
  this._redoStack = [];
114
127
  this._editingReportName = false;
@@ -2961,28 +2974,159 @@ REGLAS:
2961
2974
  `)}
2962
2975
  `;
2963
2976
  }
2964
- // ─── Data Panel ───────────────────────────────────────────────
2977
+ // ─── Data Panel (Tree + Actions) ─────────────────────────────
2965
2978
  _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
2979
  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>
2980
+ <div style="padding:6px 8px;display:flex;gap:4px;border-bottom:1px solid var(--zrd-border,#e0e0e0);flex-shrink:0;">
2981
+ ${this.dbProvider ? html `
2982
+ <button style="flex:1;padding:5px 8px;border:1px solid var(--zrd-border,#ddd);border-radius:4px;cursor:pointer;font-size:10px;font-weight:600;background:var(--zrd-input-bg,#fff);color:var(--zrd-primary,#1976d2);"
2983
+ @click=${() => { this._dbConnectorOpen = true; }}>
2984
+ \u{1F5C4} Connect DB
2985
+ </button>
2986
+ ` : nothing}
2987
+ ${this._layout.dataSources.length > 0 ? html `
2988
+ <button style="flex:1;padding:5px 8px;border:1px solid var(--zrd-border,#ddd);border-radius:4px;cursor:pointer;font-size:10px;font-weight:600;background:var(--zrd-input-bg,#fff);color:var(--zrd-text,#555);"
2989
+ @click=${() => { this._erOverlayOpen = true; }}>
2990
+ \u{1F4CA} ER Diagram
2991
+ </button>
2992
+ ` : nothing}
2993
+ </div>
2994
+ <zrd-data-source-tree
2995
+ .dataSources=${this._layout.dataSources}
2996
+ .relations=${this._layout.relations || []}
2997
+ .dataProvider=${this.dataProvider}
2998
+ @tree-action=${(e) => this._onTreeAction(e.detail)}
2999
+ @field-double-click=${(e) => this._onFieldDoubleClick(e.detail)}
3000
+ ></zrd-data-source-tree>
3001
+ ${this._renderOverlays()}
3002
+ `;
3003
+ }
3004
+ // ─── Overlays (ER Diagram + DB Connector + Relation Editor) ──
3005
+ _renderOverlays() {
3006
+ return html `
3007
+ ${this._erOverlayOpen ? this._renderErOverlay() : nothing}
3008
+ <zrd-relation-editor
3009
+ .dataSources=${this._layout.dataSources}
3010
+ .relation=${this._editingRelation}
3011
+ .open=${this._relationEditorOpen}
3012
+ @relation-save=${(e) => this._onRelationSave(e.detail.relation)}
3013
+ @relation-cancel=${() => { this._relationEditorOpen = false; this._editingRelation = null; }}
3014
+ ></zrd-relation-editor>
3015
+ <zrd-db-connector
3016
+ .open=${this._dbConnectorOpen}
3017
+ .provider=${this.dbProvider}
3018
+ @datasources-ready=${(e) => this._onDatasourcesReady(e.detail)}
3019
+ @connector-close=${() => { this._dbConnectorOpen = false; }}
3020
+ ></zrd-db-connector>
3021
+ `;
3022
+ }
3023
+ _renderErOverlay() {
3024
+ return html `
3025
+ <div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);"
3026
+ @click=${() => { this._erOverlayOpen = false; }}>
3027
+ <div style="background:var(--zrd-panel-bg,#fff);border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:85vw;height:80vh;display:flex;flex-direction:column;overflow:hidden;"
3028
+ @click=${(e) => e.stopPropagation()}>
3029
+ <div style="padding:16px 20px;border-bottom:1px solid var(--zrd-border,#eee);display:flex;justify-content:space-between;align-items:center;">
3030
+ <span style="font-size:16px;font-weight:600;">ER Diagram</span>
3031
+ <button style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--zrd-text-muted,#999);width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;"
3032
+ @click=${() => { this._erOverlayOpen = false; }}>\u2715</button>
2981
3033
  </div>
2982
- `)}
2983
- `)}
3034
+ <div style="flex:1;overflow:auto;">
3035
+ <zrd-er-diagram
3036
+ .dataSources=${this._layout.dataSources}
3037
+ .relations=${this._layout.relations || []}
3038
+ @relation-edit=${(e) => {
3039
+ this._editingRelation = e.detail.relation;
3040
+ this._relationEditorOpen = true;
3041
+ }}
3042
+ @table-focus=${() => {
3043
+ this._erOverlayOpen = false;
3044
+ this._activePanel = 'data';
3045
+ }}
3046
+ ></zrd-er-diagram>
3047
+ </div>
3048
+ </div>
3049
+ </div>
2984
3050
  `;
2985
3051
  }
3052
+ // ─── DB Connector Event Handler ────────────────────────────────
3053
+ _onDatasourcesReady(detail) {
3054
+ // Merge new data sources with existing ones
3055
+ const existingIds = new Set(this._layout.dataSources.map(ds => ds.id));
3056
+ const newSources = detail.dataSources.filter(ds => !existingIds.has(ds.id));
3057
+ if (newSources.length > 0) {
3058
+ this._layout = {
3059
+ ...this._layout,
3060
+ dataSources: [...this._layout.dataSources, ...newSources],
3061
+ };
3062
+ // Emit sample data for the host app to pick up
3063
+ this.dispatchEvent(new CustomEvent('sample-data-update', {
3064
+ detail: { sampleData: detail.sampleData },
3065
+ bubbles: true, composed: true,
3066
+ }));
3067
+ this._commitChange();
3068
+ }
3069
+ this._dbConnectorOpen = false;
3070
+ }
3071
+ // ─── Tree/Relation Event Handlers ──────────────────────────────
3072
+ _onTreeAction(detail) {
3073
+ switch (detail.action) {
3074
+ case 'create-relation':
3075
+ this._editingRelation = null;
3076
+ this._relationEditorOpen = true;
3077
+ break;
3078
+ case 'show-in-er':
3079
+ this._erOverlayOpen = true;
3080
+ break;
3081
+ case 'add-all-fields': {
3082
+ const dsId = detail.node.dataSourceId;
3083
+ if (!dsId)
3084
+ return;
3085
+ const ds = this._layout.dataSources.find(d => d.id === dsId);
3086
+ if (!ds?.fields)
3087
+ return;
3088
+ // Find or create detail band
3089
+ let detailBand = this._layout.bands.find(b => b.type === 'detail');
3090
+ if (!detailBand)
3091
+ return;
3092
+ let xOffset = 2;
3093
+ for (const f of ds.fields) {
3094
+ this._addElementToBand(detailBand.id, 'field', {
3095
+ dataSource: dsId,
3096
+ field: f.name,
3097
+ x: xOffset,
3098
+ y: 2,
3099
+ });
3100
+ xOffset += 35;
3101
+ }
3102
+ break;
3103
+ }
3104
+ }
3105
+ }
3106
+ _onFieldDoubleClick(detail) {
3107
+ // Add field to detail band at next available position
3108
+ const detailBand = this._layout.bands.find(b => b.type === 'detail');
3109
+ if (!detailBand)
3110
+ return;
3111
+ this._addElementToBand(detailBand.id, 'field', {
3112
+ dataSource: detail.dataSourceId,
3113
+ field: detail.field.name,
3114
+ });
3115
+ }
3116
+ _onRelationSave(relation) {
3117
+ const relations = [...(this._layout.relations || [])];
3118
+ const idx = relations.findIndex(r => r.id === relation.id);
3119
+ if (idx >= 0) {
3120
+ relations[idx] = relation;
3121
+ }
3122
+ else {
3123
+ relations.push(relation);
3124
+ }
3125
+ this._layout = { ...this._layout, relations };
3126
+ this._relationEditorOpen = false;
3127
+ this._editingRelation = null;
3128
+ this._commitChange();
3129
+ }
2986
3130
  // ─── Band Rendering ───────────────────────────────────────────
2987
3131
  _renderBand(band) {
2988
3132
  const bandHeightPx = this._toPx(band.height);
@@ -3004,7 +3148,7 @@ REGLAS:
3004
3148
  }}>
3005
3149
  <div class="band-header-left">
3006
3150
  <span class="band-type-icon">${BAND_ICONS[band.type] || ''}</span>
3007
- <span>${BAND_LABELS[band.type] || band.type}</span>
3151
+ <span>${this._getBandLabel(band)}</span>
3008
3152
  </div>
3009
3153
  <span style="font-weight:normal">${band.height}${this._unit}</span>
3010
3154
  </div>
@@ -3012,12 +3156,13 @@ REGLAS:
3012
3156
  style="height:${bandHeightPx}px;background-color:${bgColor};background-size:${gridSizePx}px ${gridSizePx}px;"
3013
3157
  @drop=${(e) => this._onBandDrop(e, band.id)}
3014
3158
  @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();
3159
+ @mousedown=${(e) => {
3160
+ // Only deselect if clicking directly on the band body (not on a child element)
3161
+ if (e.target === e.currentTarget) {
3162
+ this._selectedBandId = band.id;
3163
+ this._selectedElementIds = new Set();
3164
+ this.requestUpdate();
3165
+ }
3021
3166
  }}>
3022
3167
  ${band.elements.length === 0 ? html `<div class="band-empty-hint">Drag elements from Toolbox or drop fields from Data panel</div>` : nothing}
3023
3168
  ${band.elements.map(el => this._renderDesignElement(el, band.id))}
@@ -3032,6 +3177,56 @@ REGLAS:
3032
3177
  </div>
3033
3178
  `;
3034
3179
  }
3180
+ // ─── Sub-Band Helpers ──────────────────────────────────────────
3181
+ /** Generate dynamic band label with sub-band suffix (A, B, C…) */
3182
+ _getBandLabel(band) {
3183
+ const base = BAND_LABELS[band.type] || band.type;
3184
+ // Count how many bands of same type exist
3185
+ const siblingsCount = this._layout.bands.filter(b => b.type === band.type && b.visible !== false).length;
3186
+ if (siblingsCount <= 1 && (band.subBandIndex ?? 0) === 0)
3187
+ return base;
3188
+ const suffix = String.fromCharCode(65 + (band.subBandIndex ?? 0)); // A, B, C…
3189
+ return `${base} ${suffix}`;
3190
+ }
3191
+ /** Add a sub-band of the same type below the selected band */
3192
+ _addSubBand(bandId) {
3193
+ const band = this._layout.bands.find(b => b.id === bandId);
3194
+ if (!band)
3195
+ return;
3196
+ const siblings = this._layout.bands.filter(b => b.type === band.type);
3197
+ const maxIdx = Math.max(...siblings.map(b => b.subBandIndex ?? 0));
3198
+ const newBand = {
3199
+ id: `band_${Date.now()}`,
3200
+ type: band.type,
3201
+ height: band.height,
3202
+ elements: [],
3203
+ subBandIndex: maxIdx + 1,
3204
+ dataSource: band.dataSource,
3205
+ groupField: band.groupField,
3206
+ };
3207
+ // Insert after the current band
3208
+ const bands = [...this._layout.bands];
3209
+ const idx = bands.findIndex(b => b.id === bandId);
3210
+ bands.splice(idx + 1, 0, newBand);
3211
+ this._layout = { ...this._layout, bands };
3212
+ this._commitChange();
3213
+ }
3214
+ /** Remove a sub-band (only if there are multiple of same type) */
3215
+ _removeSubBand(bandId) {
3216
+ const band = this._layout.bands.find(b => b.id === bandId);
3217
+ if (!band)
3218
+ return;
3219
+ const siblings = this._layout.bands.filter(b => b.type === band.type);
3220
+ if (siblings.length <= 1)
3221
+ return; // Don't remove the last one
3222
+ this._layout = {
3223
+ ...this._layout,
3224
+ bands: this._layout.bands.filter(b => b.id !== bandId),
3225
+ };
3226
+ if (this._selectedBandId === bandId)
3227
+ this._selectedBandId = null;
3228
+ this._commitChange();
3229
+ }
3035
3230
  // ─── Element Rendering (Canvas) ───────────────────────────────
3036
3231
  _renderDesignElement(el, bandId) {
3037
3232
  const isSingle = this._selectedElementIds.size === 1 && this._selectedElementIds.has(el.id);
@@ -3073,15 +3268,10 @@ REGLAS:
3073
3268
  break;
3074
3269
  default: content = `[${el.type}]`;
3075
3270
  }
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
- `;
3271
+ // Merge default + element style (same as viewer/renderer for pixel-perfect)
3272
+ const merged = mergeStyles(this._layout.defaultStyle, style);
3273
+ const cssStyle = styleToCss(merged);
3274
+ const elStyle = `left:${xPx}px;top:${yPx}px;width:${wPx}px;height:${hPx}px;${cssStyle}`;
3085
3275
  if (el.type === 'line') {
3086
3276
  return html `
3087
3277
  <div class="element ${isSingle ? 'selected' : ''} ${isMulti ? 'multi-selected' : ''}"
@@ -3778,6 +3968,12 @@ __decorate([
3778
3968
  __decorate([
3779
3969
  property({ type: Array })
3780
3970
  ], ZenttoReportDesigner.prototype, "dataSources", void 0);
3971
+ __decorate([
3972
+ property({ attribute: false })
3973
+ ], ZenttoReportDesigner.prototype, "dataProvider", void 0);
3974
+ __decorate([
3975
+ property({ attribute: false })
3976
+ ], ZenttoReportDesigner.prototype, "dbProvider", void 0);
3781
3977
  __decorate([
3782
3978
  property({ type: Number, attribute: 'grid-snap' })
3783
3979
  ], ZenttoReportDesigner.prototype, "gridSnap", void 0);
@@ -3826,6 +4022,18 @@ __decorate([
3826
4022
  __decorate([
3827
4023
  state()
3828
4024
  ], ZenttoReportDesigner.prototype, "_activePanel", void 0);
4025
+ __decorate([
4026
+ state()
4027
+ ], ZenttoReportDesigner.prototype, "_relationEditorOpen", void 0);
4028
+ __decorate([
4029
+ state()
4030
+ ], ZenttoReportDesigner.prototype, "_editingRelation", void 0);
4031
+ __decorate([
4032
+ state()
4033
+ ], ZenttoReportDesigner.prototype, "_erOverlayOpen", void 0);
4034
+ __decorate([
4035
+ state()
4036
+ ], ZenttoReportDesigner.prototype, "_dbConnectorOpen", void 0);
3829
4037
  __decorate([
3830
4038
  state()
3831
4039
  ], ZenttoReportDesigner.prototype, "_undoStack", void 0);