@fleetbase/ember-ui 0.3.23 → 0.3.24
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/addon/components/template-builder/canvas.hbs +23 -0
- package/addon/components/template-builder/canvas.js +116 -0
- package/addon/components/template-builder/element-renderer.hbs +126 -0
- package/addon/components/template-builder/element-renderer.js +398 -0
- package/addon/components/template-builder/layers-panel.hbs +99 -0
- package/addon/components/template-builder/layers-panel.js +146 -0
- package/addon/components/template-builder/properties-panel/field.hbs +7 -0
- package/addon/components/template-builder/properties-panel/field.js +9 -0
- package/addon/components/template-builder/properties-panel/section.hbs +24 -0
- package/addon/components/template-builder/properties-panel/section.js +19 -0
- package/addon/components/template-builder/properties-panel.hbs +576 -0
- package/addon/components/template-builder/properties-panel.js +413 -0
- package/addon/components/template-builder/queries-panel.hbs +84 -0
- package/addon/components/template-builder/queries-panel.js +88 -0
- package/addon/components/template-builder/query-form.hbs +260 -0
- package/addon/components/template-builder/query-form.js +309 -0
- package/addon/components/template-builder/toolbar.hbs +134 -0
- package/addon/components/template-builder/toolbar.js +106 -0
- package/addon/components/template-builder/variable-picker.hbs +210 -0
- package/addon/components/template-builder/variable-picker.js +181 -0
- package/addon/components/template-builder.hbs +119 -0
- package/addon/components/template-builder.js +567 -0
- package/addon/helpers/string-starts-with.js +14 -0
- package/addon/services/template-builder.js +72 -0
- package/addon/styles/addon.css +1 -0
- package/addon/styles/components/badge.css +66 -12
- package/addon/styles/components/template-builder.css +297 -0
- package/addon/utils/get-currency.js +1 -1
- package/app/components/template-builder/canvas.js +1 -0
- package/app/components/template-builder/element-renderer.js +1 -0
- package/app/components/template-builder/layers-panel.js +1 -0
- package/app/components/template-builder/properties-panel/field.js +1 -0
- package/app/components/template-builder/properties-panel/section.js +1 -0
- package/app/components/template-builder/properties-panel.js +1 -0
- package/app/components/template-builder/queries-panel.js +1 -0
- package/app/components/template-builder/query-form.js +1 -0
- package/app/components/template-builder/toolbar.js +1 -0
- package/app/components/template-builder/variable-picker.js +1 -0
- package/app/components/template-builder.js +1 -0
- package/app/helpers/string-starts-with.js +1 -0
- package/app/services/template-builder.js +1 -0
- package/package.json +3 -2
- package/tsconfig.declarations.json +8 -8
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { inject as service } from '@ember/service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TemplateBuilderPropertiesPanelComponent
|
|
8
|
+
*
|
|
9
|
+
* Right-side panel that shows and edits all properties of the currently
|
|
10
|
+
* selected element. Sections are collapsed/expanded. When no element is
|
|
11
|
+
* selected, shows template-level canvas settings.
|
|
12
|
+
*
|
|
13
|
+
* @argument {Object} selectedElement - Currently selected element (or null)
|
|
14
|
+
* @argument {Object} template - The template object (for canvas settings)
|
|
15
|
+
* @argument {Array} contextSchemas - Variable schemas from the API
|
|
16
|
+
* @argument {Function} onUpdateElement - Called with (uuid, changes)
|
|
17
|
+
* @argument {Function} onUpdateTemplate - Called with changes to the template itself
|
|
18
|
+
* @argument {Function} onOpenVariablePicker - Called to open the variable picker modal
|
|
19
|
+
*/
|
|
20
|
+
export default class TemplateBuilderPropertiesPanelComponent extends Component {
|
|
21
|
+
@service fetch;
|
|
22
|
+
@service notifications;
|
|
23
|
+
|
|
24
|
+
@tracked openSections = new Set(['position', 'size', 'style', 'text', 'content']);
|
|
25
|
+
|
|
26
|
+
/** @type {Boolean} Whether an image upload is in progress */
|
|
27
|
+
@tracked isUploadingImage = false;
|
|
28
|
+
|
|
29
|
+
/** @type {String|null} Filename of the most recently uploaded image */
|
|
30
|
+
@tracked uploadedImageFilename = null;
|
|
31
|
+
|
|
32
|
+
get hasSelection() {
|
|
33
|
+
return !!this.args.selectedElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get element() {
|
|
37
|
+
return this.args.selectedElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get elementType() {
|
|
41
|
+
return this.element?.type ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get isText() {
|
|
45
|
+
return this.elementType === 'text';
|
|
46
|
+
}
|
|
47
|
+
get isImage() {
|
|
48
|
+
return this.elementType === 'image';
|
|
49
|
+
}
|
|
50
|
+
get isTable() {
|
|
51
|
+
return this.elementType === 'table';
|
|
52
|
+
}
|
|
53
|
+
get isLine() {
|
|
54
|
+
return this.elementType === 'line';
|
|
55
|
+
}
|
|
56
|
+
get isShape() {
|
|
57
|
+
return this.elementType === 'shape';
|
|
58
|
+
}
|
|
59
|
+
get isQrCode() {
|
|
60
|
+
return this.elementType === 'qr_code';
|
|
61
|
+
}
|
|
62
|
+
get isBarcode() {
|
|
63
|
+
return this.elementType === 'barcode';
|
|
64
|
+
}
|
|
65
|
+
get hasTextContent() {
|
|
66
|
+
return this.isText;
|
|
67
|
+
}
|
|
68
|
+
get hasBorderOptions() {
|
|
69
|
+
return !this.isLine;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@action
|
|
73
|
+
isSectionOpen(section) {
|
|
74
|
+
return this.openSections.has(section);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@action
|
|
78
|
+
toggleSection(section) {
|
|
79
|
+
const next = new Set(this.openSections);
|
|
80
|
+
if (next.has(section)) {
|
|
81
|
+
next.delete(section);
|
|
82
|
+
} else {
|
|
83
|
+
next.add(section);
|
|
84
|
+
}
|
|
85
|
+
this.openSections = next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@action
|
|
89
|
+
updateProp(prop, event) {
|
|
90
|
+
const value = event?.target ? event.target.value : event;
|
|
91
|
+
if (this.args.onUpdateElement && this.element) {
|
|
92
|
+
this.args.onUpdateElement(this.element.uuid, { [prop]: value });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@action
|
|
97
|
+
updateNumericProp(prop, event) {
|
|
98
|
+
const raw = event?.target ? event.target.value : event;
|
|
99
|
+
const value = raw === '' ? null : parseFloat(raw);
|
|
100
|
+
if (this.args.onUpdateElement && this.element) {
|
|
101
|
+
this.args.onUpdateElement(this.element.uuid, { [prop]: value });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@action
|
|
106
|
+
updateTemplateProp(prop, event) {
|
|
107
|
+
const value = event?.target ? event.target.value : event;
|
|
108
|
+
if (this.args.onUpdateTemplate) {
|
|
109
|
+
this.args.onUpdateTemplate({ [prop]: value });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@action
|
|
114
|
+
openVariablePicker(targetProp) {
|
|
115
|
+
if (this.args.onOpenVariablePicker) {
|
|
116
|
+
this.args.onOpenVariablePicker(targetProp, (variable) => {
|
|
117
|
+
if (this.args.onUpdateElement && this.element) {
|
|
118
|
+
const current = this.element[targetProp] ?? '';
|
|
119
|
+
this.args.onUpdateElement(this.element.uuid, { [targetProp]: current + variable });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get fontWeightOptions() {
|
|
126
|
+
return [
|
|
127
|
+
{ value: '300', label: 'Light' },
|
|
128
|
+
{ value: '400', label: 'Regular' },
|
|
129
|
+
{ value: '500', label: 'Medium' },
|
|
130
|
+
{ value: '600', label: 'Semi Bold' },
|
|
131
|
+
{ value: '700', label: 'Bold' },
|
|
132
|
+
{ value: '800', label: 'Extra Bold' },
|
|
133
|
+
{ value: '900', label: 'Black' },
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get fontFamilyOptions() {
|
|
138
|
+
return [
|
|
139
|
+
{ value: 'Inter, sans-serif', label: 'Inter' },
|
|
140
|
+
{ value: 'Arial, sans-serif', label: 'Arial' },
|
|
141
|
+
{ value: 'Helvetica, sans-serif', label: 'Helvetica' },
|
|
142
|
+
{ value: 'Georgia, serif', label: 'Georgia' },
|
|
143
|
+
{ value: 'Times New Roman, serif', label: 'Times New Roman' },
|
|
144
|
+
{ value: 'Courier New, monospace', label: 'Courier New' },
|
|
145
|
+
{ value: 'Roboto, sans-serif', label: 'Roboto' },
|
|
146
|
+
{ value: 'Open Sans, sans-serif', label: 'Open Sans' },
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get textAlignOptions() {
|
|
151
|
+
return [
|
|
152
|
+
{ value: 'left', icon: 'align-left' },
|
|
153
|
+
{ value: 'center', icon: 'align-center' },
|
|
154
|
+
{ value: 'right', icon: 'align-right' },
|
|
155
|
+
{ value: 'justify', icon: 'align-justify' },
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get lineStyleOptions() {
|
|
160
|
+
return [
|
|
161
|
+
{ value: 'solid', label: 'Solid' },
|
|
162
|
+
{ value: 'dashed', label: 'Dashed' },
|
|
163
|
+
{ value: 'dotted', label: 'Dotted' },
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get objectFitOptions() {
|
|
168
|
+
return [
|
|
169
|
+
{ value: 'cover', label: 'Cover' },
|
|
170
|
+
{ value: 'contain', label: 'Contain' },
|
|
171
|
+
{ value: 'fill', label: 'Fill' },
|
|
172
|
+
{ value: 'none', label: 'None' },
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -------------------------------------------------------------------------
|
|
177
|
+
// Table helpers
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
get tableColumns() {
|
|
181
|
+
return this.element?.columns ?? [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get tableRows() {
|
|
185
|
+
return this.element?.rows ?? [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The current data mode for the table: 'variable', 'query', or 'manual'.
|
|
190
|
+
* Stored explicitly as `data_source_mode` on the element so the mode is
|
|
191
|
+
* independent of whether the variable/query fields have been filled in yet.
|
|
192
|
+
* Defaults to 'manual' for new elements.
|
|
193
|
+
*/
|
|
194
|
+
get tableDataMode() {
|
|
195
|
+
return this.element?.data_source_mode ?? 'manual';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@action
|
|
199
|
+
setTableDataMode(mode) {
|
|
200
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
201
|
+
const changes = { data_source_mode: mode };
|
|
202
|
+
if (mode === 'manual') {
|
|
203
|
+
// Clear variable/query fields when switching to manual
|
|
204
|
+
changes.data_source = null;
|
|
205
|
+
changes.query_endpoint = null;
|
|
206
|
+
changes.query_params = [];
|
|
207
|
+
changes.query_response_path = null;
|
|
208
|
+
} else if (mode === 'variable') {
|
|
209
|
+
// Clear query fields when switching to variable
|
|
210
|
+
changes.query_endpoint = null;
|
|
211
|
+
changes.query_params = [];
|
|
212
|
+
changes.query_response_path = null;
|
|
213
|
+
} else if (mode === 'query') {
|
|
214
|
+
// Clear variable field when switching to query
|
|
215
|
+
changes.data_source = null;
|
|
216
|
+
// Seed empty query_params array if not already present
|
|
217
|
+
if (!this.element.query_params) {
|
|
218
|
+
changes.query_params = [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.args.onUpdateElement(this.element.uuid, changes);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Query data source helpers ────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
get queryParams() {
|
|
227
|
+
return this.element?.query_params ?? [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@action
|
|
231
|
+
addQueryParam() {
|
|
232
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
233
|
+
const params = [...this.queryParams, { key: '', value: '' }];
|
|
234
|
+
this.args.onUpdateElement(this.element.uuid, { query_params: params });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@action
|
|
238
|
+
removeQueryParam(index) {
|
|
239
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
240
|
+
const params = this.queryParams.filter((_, i) => i !== index);
|
|
241
|
+
this.args.onUpdateElement(this.element.uuid, { query_params: params });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@action
|
|
245
|
+
updateQueryParamKey(index, event) {
|
|
246
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
247
|
+
const params = this.queryParams.map((p, i) => (i === index ? { ...p, key: event.target.value } : p));
|
|
248
|
+
this.args.onUpdateElement(this.element.uuid, { query_params: params });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@action
|
|
252
|
+
updateQueryParamValue(index, event) {
|
|
253
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
254
|
+
const params = this.queryParams.map((p, i) => (i === index ? { ...p, value: event.target.value } : p));
|
|
255
|
+
this.args.onUpdateElement(this.element.uuid, { query_params: params });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@action
|
|
259
|
+
addColumn() {
|
|
260
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
261
|
+
const columns = [...this.tableColumns, { label: '', key: '' }];
|
|
262
|
+
this.args.onUpdateElement(this.element.uuid, { columns });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@action
|
|
266
|
+
removeColumn(index) {
|
|
267
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
268
|
+
const columns = this.tableColumns.filter((_, i) => i !== index);
|
|
269
|
+
// Also remove the corresponding key from all rows
|
|
270
|
+
const removedKey = this.tableColumns[index]?.key;
|
|
271
|
+
const rows = removedKey
|
|
272
|
+
? this.tableRows.map((row) => {
|
|
273
|
+
const next = Object.assign({}, row);
|
|
274
|
+
delete next[removedKey];
|
|
275
|
+
return next;
|
|
276
|
+
})
|
|
277
|
+
: this.tableRows;
|
|
278
|
+
this.args.onUpdateElement(this.element.uuid, { columns, rows });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@action
|
|
282
|
+
updateColumnLabel(index, event) {
|
|
283
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
284
|
+
const columns = this.tableColumns.map((col, i) => (i === index ? { ...col, label: event.target.value } : col));
|
|
285
|
+
this.args.onUpdateElement(this.element.uuid, { columns });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@action
|
|
289
|
+
updateColumnKey(index, event) {
|
|
290
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
291
|
+
const oldKey = this.tableColumns[index]?.key;
|
|
292
|
+
const newKey = event.target.value;
|
|
293
|
+
const columns = this.tableColumns.map((col, i) => (i === index ? { ...col, key: newKey } : col));
|
|
294
|
+
// Rename the key in all existing rows
|
|
295
|
+
const rows = this.tableRows.map((row) => {
|
|
296
|
+
const next = Object.assign({}, row);
|
|
297
|
+
if (oldKey && oldKey !== newKey) {
|
|
298
|
+
next[newKey] = next[oldKey] ?? '';
|
|
299
|
+
delete next[oldKey];
|
|
300
|
+
}
|
|
301
|
+
return next;
|
|
302
|
+
});
|
|
303
|
+
this.args.onUpdateElement(this.element.uuid, { columns, rows });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@action
|
|
307
|
+
addRow() {
|
|
308
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
309
|
+
// Build an empty row with a key for each defined column
|
|
310
|
+
const emptyRow = {};
|
|
311
|
+
this.tableColumns.forEach((col) => {
|
|
312
|
+
if (col.key) emptyRow[col.key] = '';
|
|
313
|
+
});
|
|
314
|
+
const rows = [...this.tableRows, emptyRow];
|
|
315
|
+
this.args.onUpdateElement(this.element.uuid, { rows });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@action
|
|
319
|
+
removeRow(index) {
|
|
320
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
321
|
+
const rows = this.tableRows.filter((_, i) => i !== index);
|
|
322
|
+
this.args.onUpdateElement(this.element.uuid, { rows });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@action
|
|
326
|
+
updateRowCell(rowIndex, key, event) {
|
|
327
|
+
if (!this.args.onUpdateElement || !this.element) return;
|
|
328
|
+
const rows = this.tableRows.map((row, i) => (i === rowIndex ? { ...row, [key]: event.target.value } : row));
|
|
329
|
+
this.args.onUpdateElement(this.element.uuid, { rows });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// -------------------------------------------------------------------------
|
|
333
|
+
// Image helpers
|
|
334
|
+
// -------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
get imageIsVariable() {
|
|
337
|
+
const src = this.element?.src ?? '';
|
|
338
|
+
return src.length > 0 && src.includes('{');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* True when the image src is a URL (uploaded file) rather than a variable token.
|
|
343
|
+
*/
|
|
344
|
+
get imageIsUploaded() {
|
|
345
|
+
const src = this.element?.src ?? '';
|
|
346
|
+
return src.length > 0 && !src.includes('{');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@action
|
|
350
|
+
async onImageFileAdded(file) {
|
|
351
|
+
// Guard against duplicate calls (ember-file-upload can fire twice)
|
|
352
|
+
if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) return;
|
|
353
|
+
|
|
354
|
+
this.isUploadingImage = true;
|
|
355
|
+
this.uploadedImageFilename = file.name;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
await this.fetch.uploadFile.perform(
|
|
359
|
+
file,
|
|
360
|
+
{
|
|
361
|
+
path: 'uploads/template-builder/images',
|
|
362
|
+
type: 'template_image',
|
|
363
|
+
},
|
|
364
|
+
(uploadedFile) => {
|
|
365
|
+
this.isUploadingImage = false;
|
|
366
|
+
// Store the URL on the element but keep the filename visible in the UI
|
|
367
|
+
if (this.args.onUpdateElement && this.element) {
|
|
368
|
+
this.args.onUpdateElement(this.element.uuid, { src: uploadedFile.url });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
this.isUploadingImage = false;
|
|
374
|
+
this.uploadedImageFilename = null;
|
|
375
|
+
if (this.notifications) {
|
|
376
|
+
this.notifications.error(`Image upload failed: ${err.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@action
|
|
382
|
+
clearImageSrc() {
|
|
383
|
+
this.uploadedImageFilename = null;
|
|
384
|
+
if (this.args.onUpdateElement && this.element) {
|
|
385
|
+
this.args.onUpdateElement(this.element.uuid, { src: '' });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
get shapeOptions() {
|
|
390
|
+
return [
|
|
391
|
+
{ value: 'rectangle', label: 'Rectangle' },
|
|
392
|
+
{ value: 'circle', label: 'Circle' },
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
get paperSizeOptions() {
|
|
397
|
+
return [
|
|
398
|
+
{ value: 'A4', label: 'A4 (210 × 297 mm)' },
|
|
399
|
+
{ value: 'A3', label: 'A3 (297 × 420 mm)' },
|
|
400
|
+
{ value: 'A5', label: 'A5 (148 × 210 mm)' },
|
|
401
|
+
{ value: 'Letter', label: 'Letter (216 × 279 mm)' },
|
|
402
|
+
{ value: 'Legal', label: 'Legal (216 × 356 mm)' },
|
|
403
|
+
{ value: 'custom', label: 'Custom' },
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
get orientationOptions() {
|
|
408
|
+
return [
|
|
409
|
+
{ value: 'portrait', label: 'Portrait' },
|
|
410
|
+
{ value: 'landscape', label: 'Landscape' },
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{{! Template Builder Queries Panel }}
|
|
2
|
+
<div class="tb-queries-panel flex flex-col h-full" ...attributes>
|
|
3
|
+
|
|
4
|
+
{{! Header }}
|
|
5
|
+
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
6
|
+
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Queries</span>
|
|
7
|
+
<button
|
|
8
|
+
type="button"
|
|
9
|
+
class="flex items-center space-x-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
|
|
10
|
+
title="Add query"
|
|
11
|
+
{{on "click" this.openAddForm}}
|
|
12
|
+
>
|
|
13
|
+
<FaIcon @icon="plus" class="w-3 h-3" />
|
|
14
|
+
<span>Add</span>
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{{! Query list }}
|
|
19
|
+
<div class="flex-1 overflow-y-auto">
|
|
20
|
+
|
|
21
|
+
{{#if this.queries.length}}
|
|
22
|
+
{{#each this.queries as |query|}}
|
|
23
|
+
<div
|
|
24
|
+
class="tb-query-row group flex items-start px-3 py-2 border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{{! Icon + text }}
|
|
27
|
+
<div class="flex-1 min-w-0 mr-2">
|
|
28
|
+
<div class="flex items-center space-x-1.5">
|
|
29
|
+
<FaIcon @icon="database" class="w-3 h-3 text-indigo-500 flex-shrink-0" />
|
|
30
|
+
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 truncate">{{query.label}}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex items-center space-x-1.5 mt-0.5">
|
|
33
|
+
<span class="font-mono text-xs text-indigo-600 dark:text-indigo-400 truncate">{{{query.variable_name}}}</span>
|
|
34
|
+
{{#if query.model_type}}
|
|
35
|
+
<span class="text-xs text-gray-400 dark:text-gray-500 truncate">{{query.model_type}}</span>
|
|
36
|
+
{{/if}}
|
|
37
|
+
{{! Show "unsaved" badge for queries created before the template was saved }}
|
|
38
|
+
{{#if (string-starts-with query.uuid "_new_")}}
|
|
39
|
+
<span class="flex-shrink-0 px-1 py-0 rounded text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">unsaved</span>
|
|
40
|
+
{{/if}}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{{! Action buttons (shown on hover) }}
|
|
45
|
+
<div class="flex items-center space-x-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
class="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
49
|
+
title="Edit query"
|
|
50
|
+
{{on "click" (fn this.openEditForm query)}}
|
|
51
|
+
>
|
|
52
|
+
<FaIcon @icon="pen" class="w-3 h-3" />
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 dark:hover:text-red-400"
|
|
57
|
+
title="Delete query"
|
|
58
|
+
{{on "click" (fn this.deleteQuery query)}}
|
|
59
|
+
>
|
|
60
|
+
<FaIcon @icon="trash" class="w-3 h-3" />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{{/each}}
|
|
65
|
+
|
|
66
|
+
{{else}}
|
|
67
|
+
<div class="flex flex-col items-center justify-center py-8 px-4 text-center">
|
|
68
|
+
<FaIcon @icon="database" class="w-6 h-6 text-gray-300 dark:text-gray-600 mb-2" />
|
|
69
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">No queries yet.</p>
|
|
70
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">Queries load data from Fleetbase models at render time and are saved with the template.</p>
|
|
71
|
+
</div>
|
|
72
|
+
{{/if}}
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{{! Query Form Modal }}
|
|
77
|
+
<TemplateBuilder::QueryForm
|
|
78
|
+
@isOpen={{this.isFormOpen}}
|
|
79
|
+
@query={{this.editingQuery}}
|
|
80
|
+
@onSave={{this.handleQuerySave}}
|
|
81
|
+
@onClose={{this.closeForm}}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TemplateBuilderQueriesPanelComponent
|
|
7
|
+
*
|
|
8
|
+
* Lists the TemplateQuery records for the current template and provides
|
|
9
|
+
* add / edit / delete operations. All mutations are propagated upward via
|
|
10
|
+
* @onQueriesChange — no API calls are made here. The parent (template-builder)
|
|
11
|
+
* includes the full queries array in the template save payload, so everything
|
|
12
|
+
* persists in one request when the user clicks Save.
|
|
13
|
+
*
|
|
14
|
+
* @argument {Array} queries - Current list of query objects (from parent state)
|
|
15
|
+
* @argument {Function} onQueriesChange - Called with the updated queries array after any mutation
|
|
16
|
+
*/
|
|
17
|
+
export default class TemplateBuilderQueriesPanelComponent extends Component {
|
|
18
|
+
@tracked isFormOpen = false;
|
|
19
|
+
@tracked editingQuery = null;
|
|
20
|
+
|
|
21
|
+
// -------------------------------------------------------------------------
|
|
22
|
+
// Computed
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
get queries() {
|
|
26
|
+
return this.args.queries ?? [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -------------------------------------------------------------------------
|
|
30
|
+
// Form open/close
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
@action
|
|
34
|
+
openAddForm() {
|
|
35
|
+
this.editingQuery = null;
|
|
36
|
+
this.isFormOpen = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@action
|
|
40
|
+
openEditForm(query) {
|
|
41
|
+
this.editingQuery = query;
|
|
42
|
+
this.isFormOpen = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@action
|
|
46
|
+
closeForm() {
|
|
47
|
+
this.isFormOpen = false;
|
|
48
|
+
this.editingQuery = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// -------------------------------------------------------------------------
|
|
52
|
+
// CRUD — all mutations notify the parent via @onQueriesChange
|
|
53
|
+
// -------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@action
|
|
56
|
+
handleQuerySave(queryData) {
|
|
57
|
+
let updated;
|
|
58
|
+
|
|
59
|
+
if (queryData.uuid) {
|
|
60
|
+
// Update existing query in the list
|
|
61
|
+
updated = this.queries.map((q) => (q.uuid === queryData.uuid ? { ...q, ...queryData } : q));
|
|
62
|
+
} else {
|
|
63
|
+
// New query — assign a temporary client-side UUID so it can be
|
|
64
|
+
// identified for edits/deletes before the template is saved.
|
|
65
|
+
const tempUuid = `_new_${Date.now()}`;
|
|
66
|
+
updated = [...this.queries, { ...queryData, uuid: tempUuid }];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this._notify(updated);
|
|
70
|
+
this.closeForm();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@action
|
|
74
|
+
deleteQuery(query) {
|
|
75
|
+
const updated = this.queries.filter((q) => q.uuid !== query.uuid);
|
|
76
|
+
this._notify(updated);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
// Private
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
_notify(queries) {
|
|
84
|
+
if (this.args.onQueriesChange) {
|
|
85
|
+
this.args.onQueriesChange(queries);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|