@fleetbase/ember-ui 0.3.22 → 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/layout/header/smart-nav-menu/dropdown.hbs +77 -68
- package/addon/components/layout/header/smart-nav-menu/dropdown.js +7 -54
- 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/smart-nav-menu.css +35 -29
- 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,260 @@
|
|
|
1
|
+
{{! Template Builder Query Form Modal }}
|
|
2
|
+
{{! Consume _syncKey so Glimmer tracks @isOpen and @query changes }}
|
|
3
|
+
{{this._syncKey}}
|
|
4
|
+
{{#if @isOpen}}
|
|
5
|
+
<div class="tb-query-form fixed inset-0 z-50 flex items-center justify-center" role="dialog" aria-modal="true">
|
|
6
|
+
|
|
7
|
+
{{! Backdrop }}
|
|
8
|
+
<div class="absolute inset-0 bg-black/40 dark:bg-black/60" {{on "click" this.cancel}}></div>
|
|
9
|
+
|
|
10
|
+
{{! Modal }}
|
|
11
|
+
<div class="relative z-10 w-full max-w-2xl bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 flex flex-col max-h-[90vh]">
|
|
12
|
+
|
|
13
|
+
{{! Header }}
|
|
14
|
+
<div class="flex items-center justify-between px-5 py-3.5 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
15
|
+
<div class="flex items-center space-x-2">
|
|
16
|
+
<FaIcon @icon="database" class="w-4 h-4 text-indigo-500" />
|
|
17
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
18
|
+
{{if this.isEditing "Edit Query" "New Query"}}
|
|
19
|
+
</h3>
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-400" {{on "click" this.cancel}}>
|
|
22
|
+
<FaIcon @icon="xmark" class="w-4 h-4" />
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
{{! Body (scrollable) }}
|
|
27
|
+
<div class="flex-1 overflow-y-auto px-5 py-4 space-y-6">
|
|
28
|
+
|
|
29
|
+
{{! ── Basic Info ── }}
|
|
30
|
+
<div>
|
|
31
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Basic Info</h4>
|
|
32
|
+
|
|
33
|
+
<div class="grid grid-cols-2 gap-3">
|
|
34
|
+
<div>
|
|
35
|
+
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
36
|
+
Label <span class="text-red-400">*</span>
|
|
37
|
+
</label>
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
class="tb-input w-full"
|
|
41
|
+
placeholder="e.g. Recent Orders"
|
|
42
|
+
value={{this.label}}
|
|
43
|
+
{{on "input" this.updateLabel}}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Variable Name</label>
|
|
48
|
+
<div class="flex items-center space-x-1">
|
|
49
|
+
<span class="text-xs text-gray-400 font-mono">{</span>
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
class="tb-input flex-1 font-mono"
|
|
53
|
+
placeholder="recent_orders"
|
|
54
|
+
value={{this.variableName}}
|
|
55
|
+
{{on "input" this.updateVariableName}}
|
|
56
|
+
/>
|
|
57
|
+
<span class="text-xs text-gray-400 font-mono">}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">Auto-derived from label if left blank.</p>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
|
65
|
+
<input
|
|
66
|
+
type="text"
|
|
67
|
+
class="tb-input w-full"
|
|
68
|
+
placeholder="Optional description"
|
|
69
|
+
value={{this.description}}
|
|
70
|
+
{{on "input" this.updateDescription}}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{{! ── Resource Type ── }}
|
|
76
|
+
<div>
|
|
77
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-1">Resource Type</h4>
|
|
78
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">Select the Fleetbase model this query runs against.</p>
|
|
79
|
+
|
|
80
|
+
<div class="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg p-2">
|
|
81
|
+
{{#each this.resourceTypes as |rt|}}
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="flex items-center space-x-2 px-2.5 py-2 rounded-lg border text-left transition-colors
|
|
85
|
+
{{if (eq this.modelType rt.value)
|
|
86
|
+
'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300'
|
|
87
|
+
'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 text-gray-700 dark:text-gray-300'}}"
|
|
88
|
+
{{on "click" (fn this.selectResourceType rt.value)}}
|
|
89
|
+
>
|
|
90
|
+
<FaIcon @icon={{rt.icon}} class="w-3.5 h-3.5 flex-shrink-0 {{if (eq this.modelType rt.value) 'text-indigo-500' 'text-gray-400'}}" />
|
|
91
|
+
<span class="block text-xs font-medium truncate">{{rt.label}}</span>
|
|
92
|
+
</button>
|
|
93
|
+
{{/each}}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{{! ── Filter Conditions ── }}
|
|
98
|
+
<div>
|
|
99
|
+
<div class="flex items-center justify-between mb-2">
|
|
100
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Filter Conditions</h4>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="flex items-center space-x-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700"
|
|
104
|
+
{{on "click" this.addCondition}}
|
|
105
|
+
>
|
|
106
|
+
<FaIcon @icon="plus" class="w-3 h-3" />
|
|
107
|
+
<span>Add condition</span>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{{#if this.conditions.length}}
|
|
112
|
+
<div class="space-y-1.5">
|
|
113
|
+
{{#each this.conditions as |condition condIndex|}}
|
|
114
|
+
{{! Each row: field (flex-1) | operator (fixed 130px) | value (flex-1) | remove }}
|
|
115
|
+
<div class="flex items-center gap-1.5">
|
|
116
|
+
<input
|
|
117
|
+
type="text"
|
|
118
|
+
class="tb-input min-w-0 flex-1"
|
|
119
|
+
placeholder="field"
|
|
120
|
+
value={{condition.field}}
|
|
121
|
+
{{on "input" (fn this.updateConditionField condIndex "field")}}
|
|
122
|
+
/>
|
|
123
|
+
<select
|
|
124
|
+
class="tb-input flex-shrink-0 tb-select-operator"
|
|
125
|
+
{{on "change" (fn this.updateConditionField condIndex "operator")}}
|
|
126
|
+
>
|
|
127
|
+
{{#each this.conditionOperators as |op|}}
|
|
128
|
+
<option value={{op.value}} selected={{eq condition.operator op.value}}>{{op.label}}</option>
|
|
129
|
+
{{/each}}
|
|
130
|
+
</select>
|
|
131
|
+
{{#if (this.showConditionValue condition.operator)}}
|
|
132
|
+
<input
|
|
133
|
+
type="text"
|
|
134
|
+
class="tb-input min-w-0 flex-1"
|
|
135
|
+
placeholder="value or {variable}"
|
|
136
|
+
value={{condition.value}}
|
|
137
|
+
{{on "input" (fn this.updateConditionField condIndex "value")}}
|
|
138
|
+
/>
|
|
139
|
+
{{/if}}
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 flex-shrink-0"
|
|
143
|
+
title="Remove condition"
|
|
144
|
+
{{on "click" (fn this.removeCondition condIndex)}}
|
|
145
|
+
>
|
|
146
|
+
<FaIcon @icon="xmark" class="w-3 h-3" />
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
{{/each}}
|
|
150
|
+
</div>
|
|
151
|
+
{{else}}
|
|
152
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 text-center py-2 border border-dashed border-gray-200 dark:border-gray-700 rounded-lg">No conditions — all records will be returned.</p>
|
|
153
|
+
{{/if}}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{{! ── Sort ── }}
|
|
157
|
+
<div>
|
|
158
|
+
<div class="flex items-center justify-between mb-2">
|
|
159
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Sort</h4>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="flex items-center space-x-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700"
|
|
163
|
+
{{on "click" this.addSort}}
|
|
164
|
+
>
|
|
165
|
+
<FaIcon @icon="plus" class="w-3 h-3" />
|
|
166
|
+
<span>Add sort</span>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{{#if this.sort.length}}
|
|
171
|
+
<div class="space-y-1.5">
|
|
172
|
+
{{#each this.sort as |sortItem sortIndex|}}
|
|
173
|
+
<div class="flex items-center gap-1.5">
|
|
174
|
+
<input
|
|
175
|
+
type="text"
|
|
176
|
+
class="tb-input min-w-0 flex-1"
|
|
177
|
+
placeholder="field (e.g. created_at)"
|
|
178
|
+
value={{sortItem.field}}
|
|
179
|
+
{{on "input" (fn this.updateSortField sortIndex "field")}}
|
|
180
|
+
/>
|
|
181
|
+
<select
|
|
182
|
+
class="tb-input flex-shrink-0 tb-select-direction"
|
|
183
|
+
{{on "change" (fn this.updateSortField sortIndex "direction")}}
|
|
184
|
+
>
|
|
185
|
+
<option value="asc" selected={{eq sortItem.direction "asc"}}>Ascending</option>
|
|
186
|
+
<option value="desc" selected={{eq sortItem.direction "desc"}}>Descending</option>
|
|
187
|
+
</select>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 flex-shrink-0"
|
|
191
|
+
title="Remove sort"
|
|
192
|
+
{{on "click" (fn this.removeSort sortIndex)}}
|
|
193
|
+
>
|
|
194
|
+
<FaIcon @icon="xmark" class="w-3 h-3" />
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
{{/each}}
|
|
198
|
+
</div>
|
|
199
|
+
{{else}}
|
|
200
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 text-center py-2 border border-dashed border-gray-200 dark:border-gray-700 rounded-lg">No sort — default model order will be used.</p>
|
|
201
|
+
{{/if}}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{{! ── Limit & Eager-load ── }}
|
|
205
|
+
<div class="grid grid-cols-2 gap-4">
|
|
206
|
+
<div>
|
|
207
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Limit</h4>
|
|
208
|
+
<input
|
|
209
|
+
type="number"
|
|
210
|
+
class="tb-input w-full"
|
|
211
|
+
placeholder="No limit"
|
|
212
|
+
min="1"
|
|
213
|
+
value={{this.limit}}
|
|
214
|
+
{{on "input" this.updateLimit}}
|
|
215
|
+
/>
|
|
216
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Max number of rows to return.</p>
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Eager Load (with)</h4>
|
|
220
|
+
<input
|
|
221
|
+
type="text"
|
|
222
|
+
class="tb-input w-full"
|
|
223
|
+
placeholder="e.g. driver,vehicle"
|
|
224
|
+
value={{this.withString}}
|
|
225
|
+
{{on "input" this.updateWith}}
|
|
226
|
+
/>
|
|
227
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Comma-separated relationship names to eager-load.</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{{! Footer }}
|
|
234
|
+
<div class="flex items-center justify-between px-5 py-3.5 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
235
|
+
{{#if this.errorMessage}}
|
|
236
|
+
<p class="text-xs text-red-500">{{this.errorMessage}}</p>
|
|
237
|
+
{{else}}
|
|
238
|
+
<div></div>
|
|
239
|
+
{{/if}}
|
|
240
|
+
<div class="flex items-center space-x-2">
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
class="px-3 py-1.5 text-xs text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
|
244
|
+
{{on "click" this.cancel}}
|
|
245
|
+
>
|
|
246
|
+
Cancel
|
|
247
|
+
</button>
|
|
248
|
+
<Button
|
|
249
|
+
@type="primary"
|
|
250
|
+
@text={{if this.isEditing "Save Changes" "Create Query"}}
|
|
251
|
+
@icon="floppy-disk"
|
|
252
|
+
@size="xs"
|
|
253
|
+
@onClick={{this.save}}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
{{/if}}
|
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
* TemplateBuilderQueryFormComponent
|
|
8
|
+
*
|
|
9
|
+
* Modal dialog for creating or editing a TemplateQuery.
|
|
10
|
+
* This component is purely presentational — it validates the form and returns
|
|
11
|
+
* the data to the parent via @onSave. No API calls are made here; the parent
|
|
12
|
+
* (template-builder) persists everything in one go when the template is saved.
|
|
13
|
+
*
|
|
14
|
+
* Form state is split into individual @tracked fields plus separate @tracked
|
|
15
|
+
* arrays for conditions and sort. This prevents the {{#each}} loops from
|
|
16
|
+
* destroying and recreating input DOM elements on every keystroke (which would
|
|
17
|
+
* steal focus from the active input).
|
|
18
|
+
*
|
|
19
|
+
* @argument {Boolean} isOpen - Whether the modal is visible
|
|
20
|
+
* @argument {Object} query - Existing query to edit (null for create)
|
|
21
|
+
* @argument {Array} resourceTypes - Optional override for the resource type list
|
|
22
|
+
* @argument {Function} onSave - Called with the validated query data object
|
|
23
|
+
* @argument {Function} onClose - Called when the modal should close
|
|
24
|
+
*/
|
|
25
|
+
export default class TemplateBuilderQueryFormComponent extends Component {
|
|
26
|
+
@service('template-builder') templateBuilderService;
|
|
27
|
+
|
|
28
|
+
// ── Scalar form fields ──────────────────────────────────────────────────
|
|
29
|
+
@tracked label = '';
|
|
30
|
+
@tracked variableName = '';
|
|
31
|
+
@tracked description = '';
|
|
32
|
+
@tracked modelType = '';
|
|
33
|
+
@tracked limit = '';
|
|
34
|
+
@tracked withRelations = [];
|
|
35
|
+
|
|
36
|
+
// ── Array fields — kept as separate tracked properties so that mutating
|
|
37
|
+
// a single item does NOT cause the entire form to re-render and destroy
|
|
38
|
+
// focused input elements. ────────────────────────────────────────────
|
|
39
|
+
@tracked conditions = [];
|
|
40
|
+
@tracked sort = [];
|
|
41
|
+
|
|
42
|
+
@tracked errorMessage = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Track whether the user has manually edited the variable_name field.
|
|
46
|
+
* While false, variable_name is auto-derived from label on every keystroke.
|
|
47
|
+
*/
|
|
48
|
+
@tracked _variableNameManuallyEdited = false;
|
|
49
|
+
|
|
50
|
+
// -------------------------------------------------------------------------
|
|
51
|
+
// Lifecycle — sync form state when @query or @isOpen changes
|
|
52
|
+
// -------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
get isEditing() {
|
|
55
|
+
return !!this.args.query?.uuid;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Glimmer re-evaluates this getter whenever @query or @isOpen changes,
|
|
59
|
+
// giving us a hook to reset the form without needing did-update modifiers.
|
|
60
|
+
get _syncKey() {
|
|
61
|
+
const key = `${this.args.isOpen}-${this.args.query?.uuid ?? 'new'}`;
|
|
62
|
+
this._syncForm();
|
|
63
|
+
return key;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_syncForm() {
|
|
67
|
+
const q = this.args.query;
|
|
68
|
+
if (q) {
|
|
69
|
+
this.label = q.label ?? '';
|
|
70
|
+
this.variableName = q.variable_name ?? '';
|
|
71
|
+
this.description = q.description ?? '';
|
|
72
|
+
this.modelType = q.model_type ?? '';
|
|
73
|
+
this.limit = q.limit ?? '';
|
|
74
|
+
this.withRelations = Array.isArray(q.with) ? [...q.with] : [];
|
|
75
|
+
this.conditions = JSON.parse(JSON.stringify(q.conditions ?? []));
|
|
76
|
+
this.sort = JSON.parse(JSON.stringify(q.sort ?? []));
|
|
77
|
+
this._variableNameManuallyEdited = !!q.variable_name?.trim();
|
|
78
|
+
} else {
|
|
79
|
+
this.label = '';
|
|
80
|
+
this.variableName = '';
|
|
81
|
+
this.description = '';
|
|
82
|
+
this.modelType = '';
|
|
83
|
+
this.limit = '';
|
|
84
|
+
this.withRelations = [];
|
|
85
|
+
this.conditions = [];
|
|
86
|
+
this.sort = [];
|
|
87
|
+
this._variableNameManuallyEdited = false;
|
|
88
|
+
}
|
|
89
|
+
this.errorMessage = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// -------------------------------------------------------------------------
|
|
93
|
+
// Resource types
|
|
94
|
+
// Priority: @resourceTypes arg > service-registered types > built-in defaults
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
get resourceTypes() {
|
|
98
|
+
if (this.args.resourceTypes?.length) {
|
|
99
|
+
return this.args.resourceTypes;
|
|
100
|
+
}
|
|
101
|
+
const serviceTypes = this.templateBuilderService?.resourceTypes ?? [];
|
|
102
|
+
if (serviceTypes.length) {
|
|
103
|
+
return serviceTypes;
|
|
104
|
+
}
|
|
105
|
+
return [
|
|
106
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Order', label: 'Order', icon: 'box' },
|
|
107
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Driver', label: 'Driver', icon: 'id-card' },
|
|
108
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Place', label: 'Place', icon: 'location-dot' },
|
|
109
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Vendor', label: 'Vendor', icon: 'building' },
|
|
110
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Contact', label: 'Contact', icon: 'address-book' },
|
|
111
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Issue', label: 'Issue', icon: 'triangle-exclamation' },
|
|
112
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\Vehicle', label: 'Vehicle', icon: 'truck' },
|
|
113
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\FuelReport', label: 'Fuel Report', icon: 'gas-pump' },
|
|
114
|
+
{ value: 'Fleetbase\\FleetOps\\Models\\PurchaseRate', label: 'Purchase Rate', icon: 'receipt' },
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
// Condition helpers
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when the condition operator requires a value input.
|
|
124
|
+
* Operators "null" and "not null" are unary — no value field is needed.
|
|
125
|
+
* Used in the template to replace the {{#unless (or ...)}} pattern that
|
|
126
|
+
* violates the simple-unless lint rule.
|
|
127
|
+
*/
|
|
128
|
+
showConditionValue(operator) {
|
|
129
|
+
return operator !== 'null' && operator !== 'not null';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// Condition operators
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
get conditionOperators() {
|
|
137
|
+
return [
|
|
138
|
+
{ value: '=', label: '= equals' },
|
|
139
|
+
{ value: '!=', label: '≠ not equals' },
|
|
140
|
+
{ value: '>', label: '> greater than' },
|
|
141
|
+
{ value: '>=', label: '≥ greater or equal' },
|
|
142
|
+
{ value: '<', label: '< less than' },
|
|
143
|
+
{ value: '<=', label: '≤ less or equal' },
|
|
144
|
+
{ value: 'like', label: '~ contains' },
|
|
145
|
+
{ value: 'not like', label: '!~ not contains' },
|
|
146
|
+
{ value: 'in', label: 'in list' },
|
|
147
|
+
{ value: 'not in', label: 'not in list' },
|
|
148
|
+
{ value: 'null', label: 'is null' },
|
|
149
|
+
{ value: 'not null', label: 'is not null' },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
// Eager-load (with) — stored as array, edited as comma string
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
get withString() {
|
|
158
|
+
return (this.withRelations ?? []).join(', ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@action
|
|
162
|
+
updateWith(event) {
|
|
163
|
+
const raw = event.target.value;
|
|
164
|
+
this.withRelations = raw
|
|
165
|
+
.split(',')
|
|
166
|
+
.map((s) => s.trim())
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
// Scalar field updates
|
|
172
|
+
// -------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
@action
|
|
175
|
+
updateLabel(event) {
|
|
176
|
+
const value = event.target.value;
|
|
177
|
+
this.label = value;
|
|
178
|
+
if (!this._variableNameManuallyEdited) {
|
|
179
|
+
this.variableName = this._deriveVariableName(value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@action
|
|
184
|
+
updateVariableName(event) {
|
|
185
|
+
this._variableNameManuallyEdited = true;
|
|
186
|
+
this.variableName = event.target.value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@action
|
|
190
|
+
updateDescription(event) {
|
|
191
|
+
this.description = event.target.value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@action
|
|
195
|
+
updateLimit(event) {
|
|
196
|
+
const raw = event.target.value;
|
|
197
|
+
this.limit = raw === '' ? '' : parseInt(raw, 10);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@action
|
|
201
|
+
selectResourceType(value) {
|
|
202
|
+
this.modelType = value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
// Conditions — mutate in-place then bump the array reference so Glimmer
|
|
207
|
+
// re-evaluates the {{#each}} without destroying existing DOM nodes.
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
@action
|
|
211
|
+
addCondition() {
|
|
212
|
+
this.conditions = [...this.conditions, { field: '', operator: '=', value: '' }];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@action
|
|
216
|
+
updateConditionField(index, field, event) {
|
|
217
|
+
const value = event.target.value;
|
|
218
|
+
// Mutate the specific item in-place — the DOM node for this input stays
|
|
219
|
+
// alive and keeps focus. Then bump the array reference so Glimmer knows
|
|
220
|
+
// the list changed (needed for the operator select and value visibility).
|
|
221
|
+
const item = this.conditions[index];
|
|
222
|
+
if (item) {
|
|
223
|
+
Object.assign(item, { [field]: value });
|
|
224
|
+
this.conditions = [...this.conditions];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@action
|
|
229
|
+
removeCondition(index) {
|
|
230
|
+
this.conditions = this.conditions.filter((_, i) => i !== index);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// -------------------------------------------------------------------------
|
|
234
|
+
// Sort — same in-place mutation pattern as conditions
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
@action
|
|
238
|
+
addSort() {
|
|
239
|
+
this.sort = [...this.sort, { field: '', direction: 'asc' }];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@action
|
|
243
|
+
updateSortField(index, field, event) {
|
|
244
|
+
const value = event.target.value;
|
|
245
|
+
const item = this.sort[index];
|
|
246
|
+
if (item) {
|
|
247
|
+
Object.assign(item, { [field]: value });
|
|
248
|
+
this.sort = [...this.sort];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@action
|
|
253
|
+
removeSort(index) {
|
|
254
|
+
this.sort = this.sort.filter((_, i) => i !== index);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
// Save / Cancel
|
|
259
|
+
// -------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
@action
|
|
262
|
+
save() {
|
|
263
|
+
this.errorMessage = null;
|
|
264
|
+
|
|
265
|
+
if (!this.label?.trim()) {
|
|
266
|
+
this.errorMessage = 'Label is required.';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!this.modelType) {
|
|
270
|
+
this.errorMessage = 'Please select a resource type.';
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const data = {
|
|
275
|
+
uuid: this.args.query?.uuid ?? null,
|
|
276
|
+
label: this.label.trim(),
|
|
277
|
+
variable_name: this.variableName.trim() || this._deriveVariableName(this.label),
|
|
278
|
+
description: this.description?.trim() ?? '',
|
|
279
|
+
model_type: this.modelType,
|
|
280
|
+
conditions: this.conditions.filter((c) => c.field?.trim()),
|
|
281
|
+
sort: this.sort.filter((s) => s.field?.trim()),
|
|
282
|
+
limit: this.limit === '' ? null : this.limit,
|
|
283
|
+
with: this.withRelations,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (this.args.onSave) {
|
|
287
|
+
this.args.onSave(data);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@action
|
|
292
|
+
cancel() {
|
|
293
|
+
this.errorMessage = null;
|
|
294
|
+
if (this.args.onClose) {
|
|
295
|
+
this.args.onClose();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// -------------------------------------------------------------------------
|
|
300
|
+
// Private
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
_deriveVariableName(label) {
|
|
304
|
+
return (label ?? '')
|
|
305
|
+
.toLowerCase()
|
|
306
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
307
|
+
.replace(/^_+|_+$/g, '');
|
|
308
|
+
}
|
|
309
|
+
}
|