@zentto/report-designer 1.6.2 → 1.6.4

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.
@@ -1,6 +1,5 @@
1
- // @zentto/report-designer — Data Source Tree Panel
2
- // Professional tree view: Connection Schema Table Fields
3
- // Works standalone (static DataSourceDef[]) and with lazy-loading (DataPanelProvider)
1
+ // @zentto/report-designer — Field Explorer (Crystal Reports-style)
2
+ // Categories: Database Fields, Formula Fields, Parameter Fields, Running Totals, Group Fields, Special Fields
4
3
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
5
4
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6
5
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -11,20 +10,17 @@ import { LitElement, html, css, nothing } from 'lit';
11
10
  import { customElement, property, state } from 'lit/decorators.js';
12
11
  // ─── Field Type Styling ───────────────────────────────────────────
13
12
  const FIELD_TYPE_COLORS = {
14
- string: '#4caf50',
15
- number: '#1976d2',
16
- currency: '#ff9800',
17
- date: '#e91e63',
18
- boolean: '#9c27b0',
19
- image: '#00bcd4',
13
+ string: '#4caf50', number: '#1976d2', currency: '#ff9800',
14
+ date: '#e91e63', boolean: '#9c27b0', image: '#00bcd4',
20
15
  };
21
- const FIELD_TYPE_ICONS = {
22
- string: 'Aa',
23
- number: '#',
24
- currency: '$',
25
- date: '\u{1D4AB}', // script D
26
- boolean: '?',
27
- image: '\u{25A3}', // square with fill
16
+ // ─── Category Icons ───────────────────────────────────────────────
17
+ const CATEGORY_ICONS = {
18
+ database: { icon: '\u{1F5C4}', color: '#1565c0' },
19
+ formula: { icon: '\u{1D465}', color: '#e65100' }, // italic x
20
+ parameter: { icon: '?\u{FE0F}', color: '#2e7d32' },
21
+ runningTotal: { icon: '\u{03A3}', color: '#7b1fa2' }, // sigma
22
+ group: { icon: '\u{229E}', color: '#00695c' },
23
+ special: { icon: '\u{2606}', color: '#37474f' },
28
24
  };
29
25
  // ─── Component ────────────────────────────────────────────────────
30
26
  let DataSourceTree = class DataSourceTree extends LitElement {
@@ -32,30 +28,19 @@ let DataSourceTree = class DataSourceTree extends LitElement {
32
28
  super(...arguments);
33
29
  this.dataSources = [];
34
30
  this.relations = [];
31
+ this.layout = null;
35
32
  this.dataProvider = null;
36
- this._tree = [];
37
33
  this._expandedIds = new Set();
38
34
  this._searchQuery = '';
39
35
  this._contextMenu = null;
40
- this._closeContextMenu = () => {
41
- this._contextMenu = null;
42
- };
36
+ this._closeContextMenu = () => { this._contextMenu = null; };
43
37
  }
44
38
  static { this.styles = css `
45
39
  :host {
46
- display: block;
40
+ display: flex; flex-direction: column;
47
41
  font-family: 'Segoe UI', Roboto, Arial, sans-serif;
48
- font-size: 12px;
49
- color: var(--zrd-text, #333);
50
- height: 100%;
51
- overflow: hidden;
52
- user-select: none;
53
- }
54
-
55
- .tree-container {
56
- display: flex;
57
- flex-direction: column;
58
- height: 100%;
42
+ font-size: 12px; color: var(--zrd-text, #333);
43
+ height: 100%; overflow: hidden; user-select: none;
59
44
  }
60
45
 
61
46
  /* ─── Search ──────────────────────────────── */
@@ -65,408 +50,434 @@ let DataSourceTree = class DataSourceTree extends LitElement {
65
50
  flex-shrink: 0;
66
51
  }
67
52
  .search-input {
68
- width: 100%;
69
- padding: 5px 8px;
70
- border: 1px solid var(--zrd-border, #ddd);
71
- border-radius: 4px;
72
- font-size: 11px;
73
- outline: none;
74
- background: var(--zrd-input-bg, #fff);
75
- color: var(--zrd-text, #333);
53
+ width: 100%; padding: 5px 8px;
54
+ border: 1px solid var(--zrd-border, #ddd); border-radius: 4px;
55
+ font-size: 11px; outline: none;
56
+ background: var(--zrd-input-bg, #fff); color: var(--zrd-text, #333);
76
57
  box-sizing: border-box;
77
58
  }
78
- .search-input:focus {
79
- border-color: var(--zrd-primary, #1976d2);
80
- }
81
- .search-input::placeholder {
82
- color: var(--zrd-text-muted, #999);
83
- }
59
+ .search-input:focus { border-color: var(--zrd-primary, #1976d2); }
84
60
 
85
61
  /* ─── Tree List ───────────────────────────── */
86
- .tree-list {
87
- flex: 1;
88
- overflow-y: auto;
89
- overflow-x: hidden;
90
- padding: 4px 0;
91
- }
62
+ .tree-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 2px 0; }
92
63
 
93
- .tree-node {
94
- display: flex;
95
- align-items: center;
96
- padding: 3px 8px;
97
- cursor: pointer;
98
- white-space: nowrap;
99
- min-height: 24px;
100
- }
101
- .tree-node:hover {
102
- background: var(--zrd-hover, rgba(25, 118, 210, 0.06));
103
- }
64
+ /* ─── Category Header ─────────────────────── */
65
+ .category {
66
+ display: flex; align-items: center;
67
+ padding: 5px 8px; cursor: pointer;
68
+ font-weight: 600; font-size: 11.5px;
69
+ border-bottom: 1px solid var(--zrd-border, #f0f0f0);
70
+ }
71
+ .category:hover { background: var(--zrd-hover, rgba(25,118,210,0.04)); }
72
+ .category-icon {
73
+ width: 18px; height: 18px; display: flex; align-items: center;
74
+ justify-content: center; margin-right: 4px; font-size: 12px;
75
+ border-radius: 3px; flex-shrink: 0;
76
+ }
77
+ .category-label { flex: 1; }
78
+ .category-count {
79
+ font-size: 9px; color: var(--zrd-text-muted, #999);
80
+ font-weight: normal; padding: 1px 5px;
81
+ background: var(--zrd-hover, #f0f0f0); border-radius: 8px;
82
+ }
83
+ .category-chevron {
84
+ font-size: 9px; color: var(--zrd-text-muted, #999);
85
+ transition: transform 0.15s; margin-right: 4px;
86
+ }
87
+ .category-chevron.open { transform: rotate(90deg); }
88
+ .category-add {
89
+ font-size: 14px; color: var(--zrd-primary, #1976d2);
90
+ cursor: pointer; margin-left: 4px; line-height: 1;
91
+ background: none; border: none; padding: 0 2px;
92
+ }
93
+ .category-add:hover { color: #0d47a1; }
104
94
 
105
- .node-chevron {
106
- width: 16px;
107
- height: 16px;
108
- display: flex;
109
- align-items: center;
110
- justify-content: center;
111
- font-size: 10px;
112
- color: var(--zrd-text-muted, #999);
113
- flex-shrink: 0;
114
- transition: transform 0.15s ease;
115
- }
116
- .node-chevron.expanded {
117
- transform: rotate(90deg);
118
- }
119
- .node-chevron.leaf {
120
- visibility: hidden;
95
+ /* ─── Data Source (table) ──────────────────── */
96
+ .ds-node {
97
+ display: flex; align-items: center;
98
+ padding: 3px 8px 3px 24px; cursor: pointer;
99
+ font-size: 11.5px;
121
100
  }
122
-
123
- .node-icon {
124
- width: 18px;
125
- height: 18px;
126
- display: flex;
127
- align-items: center;
128
- justify-content: center;
129
- margin-right: 4px;
130
- font-size: 11px;
101
+ .ds-node:hover { background: var(--zrd-hover, rgba(25,118,210,0.06)); }
102
+ .ds-icon {
103
+ width: 16px; height: 16px; display: flex; align-items: center;
104
+ justify-content: center; margin-right: 4px; font-size: 11px;
131
105
  flex-shrink: 0;
132
- border-radius: 3px;
133
- }
134
- .node-icon.connection { background: #e3f2fd; color: #1565c0; }
135
- .node-icon.schema { background: #fff3e0; color: #e65100; }
136
- .node-icon.table { background: #e8f5e9; color: #2e7d32; }
137
- .node-icon.view { background: #f3e5f5; color: #7b1fa2; }
138
- .node-icon.field { font-weight: 700; font-size: 10px; }
139
-
140
- .node-label {
141
- flex: 1;
142
- overflow: hidden;
143
- text-overflow: ellipsis;
144
- font-size: 11.5px;
145
106
  }
146
-
147
- .node-badge {
148
- font-size: 9px;
149
- padding: 1px 5px;
150
- border-radius: 3px;
107
+ .ds-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
108
+ .ds-type {
109
+ font-size: 9px; color: var(--zrd-text-muted, #aaa);
151
110
  margin-left: 4px;
152
- color: #fff;
153
- flex-shrink: 0;
154
111
  }
155
-
156
- .pk-icon {
157
- font-size: 10px;
158
- margin-right: 2px;
159
- color: #ff9800;
112
+ .ds-chevron {
113
+ font-size: 8px; color: var(--zrd-text-muted, #999);
114
+ transition: transform 0.15s; margin-right: 4px; width: 10px;
160
115
  }
116
+ .ds-chevron.open { transform: rotate(90deg); }
161
117
 
162
- .nullable-icon {
163
- font-size: 9px;
164
- color: var(--zrd-text-muted, #aaa);
165
- margin-left: 2px;
166
- }
118
+ /* ─── Field Row ───────────────────────────── */
119
+ .field-row {
120
+ display: flex; align-items: center;
121
+ padding: 2px 8px 2px 42px; cursor: grab;
122
+ font-size: 11px; min-height: 20px;
123
+ }
124
+ .field-row:hover { background: var(--zrd-hover, rgba(25,118,210,0.06)); }
125
+ .field-row:active { cursor: grabbing; }
126
+ .field-icon {
127
+ font-size: 10px; font-weight: 700; margin-right: 4px;
128
+ width: 14px; text-align: center; flex-shrink: 0;
129
+ }
130
+ .field-pk { color: #ff9800; font-size: 10px; margin-right: 2px; }
131
+ .field-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
132
+ .field-badge {
133
+ font-size: 8px; padding: 1px 4px; border-radius: 3px;
134
+ color: #fff; font-weight: 600; margin-left: 4px; flex-shrink: 0;
135
+ }
136
+ .field-nullable { font-size: 9px; color: var(--zrd-text-muted, #bbb); margin-left: 2px; }
167
137
 
168
- /* ─── Drag ────────────────────────────────── */
169
- .field-draggable {
170
- cursor: grab;
171
- }
172
- .field-draggable:active {
173
- cursor: grabbing;
138
+ /* ─── Special/Formula/Param fields ─────────── */
139
+ .special-row {
140
+ display: flex; align-items: center;
141
+ padding: 3px 8px 3px 28px; cursor: grab;
142
+ font-size: 11px;
174
143
  }
144
+ .special-row:hover { background: var(--zrd-hover, rgba(25,118,210,0.06)); }
145
+ .special-icon { margin-right: 6px; font-size: 12px; }
175
146
 
176
147
  /* ─── Context Menu ────────────────────────── */
177
148
  .context-menu {
178
- position: fixed;
179
- background: var(--zrd-panel-bg, #fff);
180
- border: 1px solid var(--zrd-border, #ddd);
181
- border-radius: 6px;
182
- box-shadow: 0 4px 16px rgba(0,0,0,0.15);
183
- padding: 4px 0;
184
- min-width: 160px;
185
- z-index: 10000;
186
- }
187
- .context-menu-item {
188
- padding: 6px 14px;
189
- cursor: pointer;
190
- font-size: 11.5px;
191
- display: flex;
192
- align-items: center;
193
- gap: 8px;
194
- }
195
- .context-menu-item:hover {
196
- background: var(--zrd-hover, rgba(25, 118, 210, 0.08));
149
+ position: fixed; background: var(--zrd-panel-bg, #fff);
150
+ border: 1px solid var(--zrd-border, #ddd); border-radius: 6px;
151
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15); padding: 4px 0;
152
+ min-width: 160px; z-index: 10000;
197
153
  }
198
- .context-menu-divider {
199
- height: 1px;
200
- background: var(--zrd-border, #e0e0e0);
201
- margin: 4px 0;
154
+ .cm-item {
155
+ padding: 5px 14px; cursor: pointer; font-size: 11px;
156
+ display: flex; align-items: center; gap: 6px;
202
157
  }
158
+ .cm-item:hover { background: var(--zrd-hover, rgba(25,118,210,0.08)); }
159
+ .cm-divider { height: 1px; background: var(--zrd-border, #e0e0e0); margin: 3px 0; }
203
160
 
204
- /* ─── Empty State ─────────────────────────── */
205
- .empty-state {
206
- padding: 16px 12px;
207
- text-align: center;
208
- color: var(--zrd-text-muted, #999);
209
- font-size: 11.5px;
210
- line-height: 1.5;
161
+ /* ─── Relation indicator ──────────────────── */
162
+ .rel-badge {
163
+ font-size: 9px; color: var(--zrd-primary, #1976d2); margin-left: 3px;
211
164
  }
212
165
 
213
- .loading-indicator {
214
- display: inline-block;
215
- width: 12px;
216
- height: 12px;
217
- border: 2px solid var(--zrd-border, #ddd);
218
- border-top-color: var(--zrd-primary, #1976d2);
219
- border-radius: 50%;
220
- animation: spin 0.6s linear infinite;
221
- }
222
- @keyframes spin { to { transform: rotate(360deg); } }
223
-
224
- /* ─── Relations Badge ─────────────────────── */
225
- .relation-indicator {
226
- font-size: 9px;
227
- color: var(--zrd-primary, #1976d2);
228
- margin-left: 4px;
166
+ .empty-hint {
167
+ padding: 10px 12px; text-align: center;
168
+ color: var(--zrd-text-muted, #999); font-size: 11px; line-height: 1.5;
229
169
  }
230
170
  `; }
231
171
  // ─── Lifecycle ────────────────────────────────────────────────
232
172
  connectedCallback() {
233
173
  super.connectedCallback();
234
174
  document.addEventListener('click', this._closeContextMenu);
175
+ // Auto-expand Database Fields by default
176
+ this._expandedIds.add('cat::database');
235
177
  }
236
178
  disconnectedCallback() {
237
179
  super.disconnectedCallback();
238
180
  document.removeEventListener('click', this._closeContextMenu);
239
181
  }
240
- updated(changed) {
241
- if (changed.has('dataSources') || changed.has('relations')) {
242
- this._buildTree();
243
- }
244
- }
245
- // ─── Tree Building ────────────────────────────────────────────
246
- _buildTree() {
247
- const tree = [];
248
- // Group data sources by connectionId (or 'local' for static)
249
- const byConnection = new Map();
250
- for (const ds of this.dataSources) {
251
- const connId = ds.connectionId || 'local';
252
- if (!byConnection.has(connId))
253
- byConnection.set(connId, []);
254
- byConnection.get(connId).push(ds);
255
- }
256
- for (const [connId, sources] of byConnection) {
257
- // Group by schema
258
- const bySchema = new Map();
259
- for (const ds of sources) {
260
- const schema = ds.schema || 'default';
261
- if (!bySchema.has(schema))
262
- bySchema.set(schema, []);
263
- bySchema.get(schema).push(ds);
264
- }
265
- const schemaNodes = [];
266
- for (const [schemaName, schemaSources] of bySchema) {
267
- const tableNodes = schemaSources.map(ds => {
268
- const isView = ds.table?.toLowerCase().startsWith('v_') || ds.table?.toLowerCase().startsWith('vw_');
269
- const fieldNodes = (ds.fields || []).map(f => ({
270
- id: `${ds.id}::${f.name}`,
271
- label: f.label || f.name,
272
- icon: FIELD_TYPE_ICONS[f.type] || 'F',
273
- type: 'field',
274
- fieldDef: f,
275
- dataSourceId: ds.id,
276
- }));
277
- return {
278
- id: `table::${ds.id}`,
279
- label: ds.table || ds.name,
280
- icon: isView ? '\u{1F441}' : '\u{1F4CB}',
281
- type: (isView ? 'view' : 'table'),
282
- children: fieldNodes,
283
- dataSourceId: ds.id,
284
- schemaName: ds.schema,
285
- tableName: ds.table || ds.name,
286
- connectionId: ds.connectionId,
287
- };
288
- });
289
- if (bySchema.size > 1 || schemaName !== 'default') {
290
- schemaNodes.push({
291
- id: `schema::${connId}::${schemaName}`,
292
- label: schemaName,
293
- icon: '\u{1F4C1}',
294
- type: 'schema',
295
- children: tableNodes,
296
- });
297
- }
298
- else {
299
- schemaNodes.push(...tableNodes);
300
- }
301
- }
302
- if (byConnection.size > 1) {
303
- tree.push({
304
- id: `conn::${connId}`,
305
- label: connId === 'local' ? 'Local Data' : connId,
306
- icon: '\u{1F5C4}',
307
- type: 'connection',
308
- children: schemaNodes,
309
- connectionId: connId,
310
- });
311
- }
312
- else {
313
- tree.push(...schemaNodes);
314
- }
315
- }
316
- this._tree = tree;
317
- }
318
- // ─── Filtering ────────────────────────────────────────────────
319
- _matchesSearch(node, query) {
320
- if (!query)
321
- return true;
322
- const q = query.toLowerCase();
323
- if (node.label.toLowerCase().includes(q))
324
- return true;
325
- if (node.children) {
326
- return node.children.some(c => this._matchesSearch(c, q));
327
- }
328
- return false;
182
+ // ─── Toggle / Drag ────────────────────────────────────────────
183
+ _toggle(id) {
184
+ const s = new Set(this._expandedIds);
185
+ if (s.has(id))
186
+ s.delete(id);
187
+ else
188
+ s.add(id);
189
+ this._expandedIds = s;
329
190
  }
330
- // ─── Event Handlers ───────────────────────────────────────────
331
- _toggleNode(nodeId) {
332
- const expanded = new Set(this._expandedIds);
333
- if (expanded.has(nodeId)) {
334
- expanded.delete(nodeId);
335
- }
336
- else {
337
- expanded.add(nodeId);
338
- }
339
- this._expandedIds = expanded;
340
- }
341
- _onFieldDragStart(e, node) {
342
- if (!node.fieldDef || !node.dataSourceId)
343
- return;
191
+ _onFieldDrag(e, dsId, fieldName) {
344
192
  e.dataTransfer?.setData('element-type', 'field');
345
- e.dataTransfer?.setData('field-ds', node.dataSourceId);
346
- e.dataTransfer?.setData('field-name', node.fieldDef.name);
193
+ e.dataTransfer?.setData('field-ds', dsId);
194
+ e.dataTransfer?.setData('field-name', fieldName);
195
+ e.dataTransfer.effectAllowed = 'copy';
196
+ }
197
+ _onSpecialDrag(e, type) {
198
+ e.dataTransfer?.setData('element-type', type);
347
199
  e.dataTransfer.effectAllowed = 'copy';
348
- this.dispatchEvent(new CustomEvent('field-drag-start', {
349
- detail: { dataSourceId: node.dataSourceId, field: node.fieldDef },
350
- bubbles: true, composed: true,
351
- }));
352
200
  }
353
- _onFieldDoubleClick(node) {
354
- if (!node.fieldDef || !node.dataSourceId)
355
- return;
201
+ _onFieldDblClick(dsId, fieldName) {
356
202
  this.dispatchEvent(new CustomEvent('field-double-click', {
357
- detail: { dataSourceId: node.dataSourceId, field: node.fieldDef },
203
+ detail: { dataSourceId: dsId, field: { name: fieldName } },
358
204
  bubbles: true, composed: true,
359
205
  }));
360
206
  }
361
- _onContextMenu(e, node) {
207
+ // ─── Context Menu ─────────────────────────────────────────────
208
+ _onContextMenu(e, dsId, category) {
362
209
  e.preventDefault();
363
210
  e.stopPropagation();
364
- this._contextMenu = { x: e.clientX, y: e.clientY, node };
211
+ this._contextMenu = { x: e.clientX, y: e.clientY, dsId, category };
365
212
  }
366
213
  _contextAction(action) {
367
- const node = this._contextMenu?.node;
368
- if (!node)
369
- return;
214
+ const ctx = this._contextMenu;
370
215
  this._contextMenu = null;
371
216
  this.dispatchEvent(new CustomEvent('tree-action', {
372
- detail: { action, node },
217
+ detail: { action, node: { dataSourceId: ctx?.dsId, category: ctx?.category } },
373
218
  bubbles: true, composed: true,
374
219
  }));
375
220
  }
376
- // ─── Relation Helpers ─────────────────────────────────────────
377
- _getRelationsForDs(dsId) {
378
- return (this.relations || []).filter(r => r.parentSource === dsId || r.childSource === dsId);
221
+ // ─── Search ───────────────────────────────────────────────────
222
+ _matches(label) {
223
+ if (!this._searchQuery)
224
+ return true;
225
+ return label.toLowerCase().includes(this._searchQuery.toLowerCase());
379
226
  }
380
- // ─── Rendering ────────────────────────────────────────────────
381
- _renderNode(node, depth = 0) {
382
- if (this._searchQuery && !this._matchesSearch(node, this._searchQuery)) {
383
- return nothing;
227
+ // ─── Relation count ───────────────────────────────────────────
228
+ _relCount(dsId) {
229
+ return (this.relations || []).filter(r => r.parentSource === dsId || r.childSource === dsId).length;
230
+ }
231
+ // ─── Render ───────────────────────────────────────────────────
232
+ render() {
233
+ const layout = this.layout;
234
+ const ds = this.dataSources;
235
+ const params = layout?.parameters || [];
236
+ const runningTotals = layout?.runningTotals || [];
237
+ const groups = layout?.groups || [];
238
+ // Formula fields = any FieldElement with expression
239
+ const formulaFields = [];
240
+ for (const band of (layout?.bands || [])) {
241
+ for (const el of band.elements) {
242
+ if (el.type === 'field' && el.expression) {
243
+ formulaFields.push({ dsId: el.dataSource, field: el.field, expression: el.expression });
244
+ }
245
+ }
384
246
  }
385
- const isExpanded = this._expandedIds.has(node.id) ||
386
- (this._searchQuery && this._matchesSearch(node, this._searchQuery));
387
- const hasChildren = node.children && node.children.length > 0;
388
- const isField = node.type === 'field';
389
- const paddingLeft = 8 + depth * 14;
390
- const relations = node.dataSourceId ? this._getRelationsForDs(node.dataSourceId) : [];
391
247
  return html `
392
- <div
393
- class="tree-node ${isField ? 'field-draggable' : ''}"
394
- style="padding-left:${paddingLeft}px"
395
- draggable=${isField ? 'true' : 'false'}
396
- @dragstart=${isField ? (e) => this._onFieldDragStart(e, node) : nothing}
397
- @dblclick=${isField ? () => this._onFieldDoubleClick(node) : nothing}
398
- @click=${hasChildren ? () => this._toggleNode(node.id) : nothing}
399
- @contextmenu=${(node.type === 'table' || node.type === 'view')
400
- ? (e) => this._onContextMenu(e, node)
401
- : nothing}
402
- >
403
- <span class="node-chevron ${isExpanded ? 'expanded' : ''} ${!hasChildren ? 'leaf' : ''}">
404
- \u25B6
405
- </span>
406
- <span class="node-icon ${node.type}"
407
- style="${isField && node.fieldDef ? `color:${FIELD_TYPE_COLORS[node.fieldDef.type] || '#666'}` : ''}">
408
- ${node.icon}
409
- </span>
410
- ${isField && node.fieldDef?.isPrimaryKey ? html `<span class="pk-icon">\u{1F511}</span>` : nothing}
411
- <span class="node-label">${node.label}</span>
412
- ${isField && node.fieldDef ? html `
413
- <span class="node-badge" style="background:${FIELD_TYPE_COLORS[node.fieldDef.type] || '#999'}">
414
- ${node.fieldDef.nativeType || node.fieldDef.type}
415
- </span>
416
- ${node.fieldDef.nullable ? html `<span class="nullable-icon">?</span>` : nothing}
417
- ` : nothing}
418
- ${(node.type === 'table' || node.type === 'view') && relations.length > 0
419
- ? html `<span class="relation-indicator">\u{1F517}${relations.length}</span>`
420
- : nothing}
421
- ${node.loading ? html `<span class="loading-indicator"></span>` : nothing}
248
+ <div class="search-box">
249
+ <input class="search-input" type="text" placeholder="Search fields..."
250
+ .value=${this._searchQuery}
251
+ @input=${(e) => { this._searchQuery = e.target.value; }} />
252
+ </div>
253
+ <div class="tree-list">
254
+ ${this._renderDatabaseFields(ds)}
255
+ ${this._renderFormulaFields(formulaFields)}
256
+ ${this._renderParameterFields(params)}
257
+ ${this._renderRunningTotals(runningTotals)}
258
+ ${this._renderGroupFields(groups)}
259
+ ${this._renderSpecialFields()}
422
260
  </div>
423
- ${isExpanded && hasChildren
424
- ? node.children.map(child => this._renderNode(child, depth + 1))
425
- : nothing}
261
+ ${this._renderContextMenu()}
426
262
  `;
427
263
  }
428
- _renderContextMenu() {
429
- if (!this._contextMenu)
264
+ // ─── 1. Database Fields ───────────────────────────────────────
265
+ _renderDatabaseFields(dataSources) {
266
+ const catId = 'cat::database';
267
+ const isOpen = this._expandedIds.has(catId);
268
+ const cat = CATEGORY_ICONS.database;
269
+ return html `
270
+ <div class="category" @click=${() => this._toggle(catId)}
271
+ @contextmenu=${(e) => this._onContextMenu(e, undefined, 'database')}>
272
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
273
+ <span class="category-icon" style="background:#e3f2fd;color:${cat.color}">${cat.icon}</span>
274
+ <span class="category-label">Database Fields</span>
275
+ <span class="category-count">${dataSources.length}</span>
276
+ <button class="category-add" title="Add Data Source"
277
+ @click=${(e) => { e.stopPropagation(); this._contextAction('add-datasource'); }}>+</button>
278
+ </div>
279
+ ${isOpen ? html `
280
+ ${dataSources.length === 0
281
+ ? html `<div class="empty-hint">No data sources.<br/>Click + to connect a database, Zentto API, or REST endpoint.</div>`
282
+ : dataSources.map(ds => this._renderDataSource(ds))}
283
+ ` : nothing}
284
+ `;
285
+ }
286
+ _renderDataSource(ds) {
287
+ const nodeId = `ds::${ds.id}`;
288
+ const isOpen = this._expandedIds.has(nodeId) || !!this._searchQuery;
289
+ const fields = ds.fields || [];
290
+ const rels = this._relCount(ds.id);
291
+ const isView = ds.table?.toLowerCase().startsWith('v_') || ds.table?.toLowerCase().startsWith('vw_');
292
+ const icon = isView ? '\u{1F441}' : '\u{1F4CB}';
293
+ const visibleFields = fields.filter(f => this._matches(f.label || f.name));
294
+ if (this._searchQuery && visibleFields.length === 0 && !this._matches(ds.name))
430
295
  return nothing;
431
- const { x, y, node } = this._contextMenu;
432
296
  return html `
433
- <div class="context-menu" style="left:${x}px;top:${y}px"
434
- @click=${(e) => e.stopPropagation()}>
435
- <div class="context-menu-item" @click=${() => this._contextAction('preview-data')}>
436
- \u{1F50D} Preview Data
437
- </div>
438
- <div class="context-menu-item" @click=${() => this._contextAction('add-all-fields')}>
439
- \u2795 Add All Fields to Detail
440
- </div>
441
- <div class="context-menu-divider"></div>
442
- <div class="context-menu-item" @click=${() => this._contextAction('create-relation')}>
443
- \u{1F517} Create Relation...
444
- </div>
445
- <div class="context-menu-item" @click=${() => this._contextAction('show-in-er')}>
446
- \u{1F4CA} Show in ER Diagram
447
- </div>
297
+ <div class="ds-node" @click=${() => this._toggle(nodeId)}
298
+ @contextmenu=${(e) => this._onContextMenu(e, ds.id)}>
299
+ <span class="ds-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
300
+ <span class="ds-icon">${icon}</span>
301
+ <span class="ds-label">${ds.table || ds.name}</span>
302
+ <span class="ds-type">${ds.type}</span>
303
+ ${rels > 0 ? html `<span class="rel-badge">\u{1F517}${rels}</span>` : nothing}
448
304
  </div>
305
+ ${isOpen ? visibleFields.map(f => this._renderField(ds.id, f)) : nothing}
449
306
  `;
450
307
  }
451
- render() {
308
+ _renderField(dsId, f) {
309
+ const color = FIELD_TYPE_COLORS[f.type] || '#666';
452
310
  return html `
453
- <div class="tree-container">
454
- <div class="search-box">
455
- <input
456
- class="search-input"
457
- type="text"
458
- placeholder="Search fields..."
459
- .value=${this._searchQuery}
460
- @input=${(e) => { this._searchQuery = e.target.value; }}
461
- />
462
- </div>
463
- <div class="tree-list">
464
- ${this._tree.length === 0
465
- ? html `<div class="empty-state">No data sources configured.<br/>Connect a database or add data sources to get started.</div>`
466
- : this._tree.map(node => this._renderNode(node))}
311
+ <div class="field-row" draggable="true"
312
+ @dragstart=${(e) => this._onFieldDrag(e, dsId, f.name)}
313
+ @dblclick=${() => this._onFieldDblClick(dsId, f.name)}>
314
+ ${f.isPrimaryKey ? html `<span class="field-pk">\u{1F511}</span>` : nothing}
315
+ <span class="field-icon" style="color:${color}">\u{25C6}</span>
316
+ <span class="field-label">${f.label || f.name}</span>
317
+ <span class="field-badge" style="background:${color}">${f.nativeType || f.type}</span>
318
+ ${f.nullable ? html `<span class="field-nullable">?</span>` : nothing}
319
+ </div>
320
+ `;
321
+ }
322
+ // ─── 2. Formula Fields ────────────────────────────────────────
323
+ _renderFormulaFields(formulas) {
324
+ const catId = 'cat::formula';
325
+ const isOpen = this._expandedIds.has(catId);
326
+ const cat = CATEGORY_ICONS.formula;
327
+ return html `
328
+ <div class="category" @click=${() => this._toggle(catId)}>
329
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
330
+ <span class="category-icon" style="background:#fff3e0;color:${cat.color}">${cat.icon}</span>
331
+ <span class="category-label">Formula Fields</span>
332
+ <span class="category-count">${formulas.length}</span>
333
+ <button class="category-add" title="New Formula"
334
+ @click=${(e) => { e.stopPropagation(); this._contextAction('add-formula'); }}>+</button>
335
+ </div>
336
+ ${isOpen ? html `
337
+ ${formulas.length === 0
338
+ ? html `<div class="empty-hint" style="padding:6px 28px;font-size:10px;text-align:left;">No formula fields. Add expressions to field elements.</div>`
339
+ : formulas.map(f => html `
340
+ <div class="special-row">
341
+ <span class="special-icon" style="color:${cat.color}">${cat.icon}</span>
342
+ <span class="field-label">${f.field}</span>
343
+ <span style="font-size:9px;color:#999;margin-left:4px">${f.expression}</span>
344
+ </div>
345
+ `)}
346
+ ` : nothing}
347
+ `;
348
+ }
349
+ // ─── 3. Parameter Fields ──────────────────────────────────────
350
+ _renderParameterFields(params) {
351
+ const catId = 'cat::parameter';
352
+ const isOpen = this._expandedIds.has(catId);
353
+ const cat = CATEGORY_ICONS.parameter;
354
+ return html `
355
+ <div class="category" @click=${() => this._toggle(catId)}>
356
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
357
+ <span class="category-icon" style="background:#e8f5e9;color:${cat.color}">${cat.icon}</span>
358
+ <span class="category-label">Parameter Fields</span>
359
+ <span class="category-count">${params.length}</span>
360
+ <button class="category-add" title="New Parameter"
361
+ @click=${(e) => { e.stopPropagation(); this._contextAction('add-parameter'); }}>+</button>
362
+ </div>
363
+ ${isOpen ? html `
364
+ ${params.length === 0
365
+ ? html `<div class="empty-hint" style="padding:6px 28px;font-size:10px;text-align:left;">No parameters defined.</div>`
366
+ : params.map(p => html `
367
+ <div class="special-row" draggable="true"
368
+ @dragstart=${(e) => this._onSpecialDrag(e, 'parameter')}>
369
+ <span class="special-icon" style="color:${cat.color}">?</span>
370
+ <span class="field-label">${p.label || p.name}</span>
371
+ <span class="field-badge" style="background:${cat.color}">${p.type}</span>
372
+ </div>
373
+ `)}
374
+ ` : nothing}
375
+ `;
376
+ }
377
+ // ─── 4. Running Totals ────────────────────────────────────────
378
+ _renderRunningTotals(totals) {
379
+ const catId = 'cat::runningTotal';
380
+ const isOpen = this._expandedIds.has(catId);
381
+ const cat = CATEGORY_ICONS.runningTotal;
382
+ return html `
383
+ <div class="category" @click=${() => this._toggle(catId)}>
384
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
385
+ <span class="category-icon" style="background:#f3e5f5;color:${cat.color}">${cat.icon}</span>
386
+ <span class="category-label">Running Total Fields</span>
387
+ <span class="category-count">${totals.length}</span>
388
+ <button class="category-add" title="New Running Total"
389
+ @click=${(e) => { e.stopPropagation(); this._contextAction('add-running-total'); }}>+</button>
390
+ </div>
391
+ ${isOpen ? html `
392
+ ${totals.length === 0
393
+ ? html `<div class="empty-hint" style="padding:6px 28px;font-size:10px;text-align:left;">No running totals defined.</div>`
394
+ : totals.map(rt => html `
395
+ <div class="special-row">
396
+ <span class="special-icon" style="color:${cat.color}">\u{03A3}</span>
397
+ <span class="field-label">${rt.field} (${rt.summarize})</span>
398
+ </div>
399
+ `)}
400
+ ` : nothing}
401
+ `;
402
+ }
403
+ // ─── 5. Group Fields ──────────────────────────────────────────
404
+ _renderGroupFields(groups) {
405
+ const catId = 'cat::group';
406
+ const isOpen = this._expandedIds.has(catId);
407
+ const cat = CATEGORY_ICONS.group;
408
+ return html `
409
+ <div class="category" @click=${() => this._toggle(catId)}>
410
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
411
+ <span class="category-icon" style="background:#e0f2f1;color:${cat.color}">${cat.icon}</span>
412
+ <span class="category-label">Group Name Fields</span>
413
+ <span class="category-count">${groups.length}</span>
414
+ </div>
415
+ ${isOpen ? html `
416
+ ${groups.length === 0
417
+ ? html `<div class="empty-hint" style="padding:6px 28px;font-size:10px;text-align:left;">No groups defined.</div>`
418
+ : groups.map(g => html `
419
+ <div class="special-row">
420
+ <span class="special-icon" style="color:${cat.color}">\u{229E}</span>
421
+ <span class="field-label">${g.field}</span>
422
+ <span style="font-size:9px;color:#999;margin-left:4px">${g.sort || 'asc'}</span>
423
+ </div>
424
+ `)}
425
+ ` : nothing}
426
+ `;
427
+ }
428
+ // ─── 6. Special Fields ────────────────────────────────────────
429
+ _renderSpecialFields() {
430
+ const catId = 'cat::special';
431
+ const isOpen = this._expandedIds.has(catId);
432
+ const cat = CATEGORY_ICONS.special;
433
+ const specialFields = [
434
+ { type: 'pageNumber', label: 'Page Number', icon: '#' },
435
+ { type: 'totalPages', label: 'Total Pages', icon: '##' },
436
+ { type: 'currentDate', label: 'Print Date', icon: '\u{1F4C5}' },
437
+ { type: 'text', label: 'Text Object', icon: 'T' },
438
+ { type: 'line', label: 'Line', icon: '\u2500' },
439
+ { type: 'rect', label: 'Box', icon: '\u25AD' },
440
+ { type: 'image', label: 'Picture', icon: '\u{1F5BC}' },
441
+ { type: 'barcode', label: 'Barcode', icon: '\u{2584}' },
442
+ ];
443
+ return html `
444
+ <div class="category" @click=${() => this._toggle(catId)}>
445
+ <span class="category-chevron ${isOpen ? 'open' : ''}">\u25B6</span>
446
+ <span class="category-icon" style="background:#eceff1;color:${cat.color}">${cat.icon}</span>
447
+ <span class="category-label">Special Fields</span>
448
+ <span class="category-count">${specialFields.length}</span>
449
+ </div>
450
+ ${isOpen ? specialFields.map(sf => html `
451
+ <div class="special-row" draggable="true"
452
+ @dragstart=${(e) => this._onSpecialDrag(e, sf.type)}>
453
+ <span class="special-icon">${sf.icon}</span>
454
+ <span class="field-label">${sf.label}</span>
467
455
  </div>
456
+ `) : nothing}
457
+ `;
458
+ }
459
+ // ─── Context Menu ─────────────────────────────────────────────
460
+ _renderContextMenu() {
461
+ if (!this._contextMenu)
462
+ return nothing;
463
+ const { x, y, dsId, category } = this._contextMenu;
464
+ return html `
465
+ <div class="context-menu" style="left:${x}px;top:${y}px"
466
+ @click=${(e) => e.stopPropagation()}>
467
+ ${dsId ? html `
468
+ <div class="cm-item" @click=${() => this._contextAction('preview-data')}>\u{1F50D} Preview Data</div>
469
+ <div class="cm-item" @click=${() => this._contextAction('add-all-fields')}>\u2795 Add All Fields</div>
470
+ <div class="cm-divider"></div>
471
+ <div class="cm-item" @click=${() => this._contextAction('create-relation')}>\u{1F517} Create Relation...</div>
472
+ <div class="cm-item" @click=${() => this._contextAction('show-in-er')}>\u{1F4CA} Show in ER Diagram</div>
473
+ <div class="cm-divider"></div>
474
+ <div class="cm-item" @click=${() => this._contextAction('remove-datasource')}>\u{1F5D1} Remove Data Source</div>
475
+ ` : html `
476
+ <div class="cm-item" @click=${() => this._contextAction('add-datasource')}>\u{1F5C4} Connect Database...</div>
477
+ <div class="cm-item" @click=${() => this._contextAction('add-zentto')}>\u{1F310} Zentto Login...</div>
478
+ <div class="cm-item" @click=${() => this._contextAction('add-rest')}>\u{1F517} REST API...</div>
479
+ `}
468
480
  </div>
469
- ${this._renderContextMenu()}
470
481
  `;
471
482
  }
472
483
  };
@@ -476,12 +487,12 @@ __decorate([
476
487
  __decorate([
477
488
  property({ type: Array })
478
489
  ], DataSourceTree.prototype, "relations", void 0);
490
+ __decorate([
491
+ property({ type: Object })
492
+ ], DataSourceTree.prototype, "layout", void 0);
479
493
  __decorate([
480
494
  property({ attribute: false })
481
495
  ], DataSourceTree.prototype, "dataProvider", void 0);
482
- __decorate([
483
- state()
484
- ], DataSourceTree.prototype, "_tree", void 0);
485
496
  __decorate([
486
497
  state()
487
498
  ], DataSourceTree.prototype, "_expandedIds", void 0);