@5minds/node-red-dashboard-2-processcube-dynamic-table 2.0.3-develop-8a7c9e-mets8ufl → 2.0.3-develop-e1e056-mewuxjwr

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,46 +1,63 @@
1
1
  <template>
2
2
  <div class="ui-dynamic-table-container">
3
- <UIDynamicTableTitleText v-if="props.title_text && props.title_text.length > 0" :title="props.title_text"
4
- :style="props.title_style || 'default'" :customStyles="props.title_custom_text_styling || ''"
5
- :titleIcon="props.title_icon || ''" />
6
-
3
+ <UIDynamicTableTitleText
4
+ v-if="props.title_text && props.title_text.length > 0"
5
+ :title="props.title_text"
6
+ :style="props.title_style || 'default'"
7
+ :customStyles="props.title_custom_text_styling || ''"
8
+ :titleIcon="props.title_icon || ''"
9
+ />
10
+
7
11
  <!-- Search and Reload Controls -->
8
12
  <v-toolbar flat class="pb-4 pt-8">
9
- <v-text-field
10
- class="search-input"
13
+ <v-text-field
14
+ class="search-input"
11
15
  :class="{ 'mobile-search': isMobile }"
12
- v-model="search"
16
+ v-model="search"
13
17
  label="Suchen"
14
- prepend-inner-icon="mdi-magnify"
15
- variant="outlined"
16
- hide-details
17
- single-line>
18
+ prepend-inner-icon="mdi-magnify"
19
+ variant="outlined"
20
+ hide-details
21
+ single-line
22
+ >
18
23
  </v-text-field>
19
- <v-btn class="reload-button ml-4" @click="reloadData" :disabled="isReloading" size="small"
20
- variant="outlined">
24
+ <v-btn
25
+ class="reload-button ml-4"
26
+ @click="reloadData"
27
+ :disabled="isReloading"
28
+ size="small"
29
+ variant="outlined"
30
+ >
21
31
  <v-icon left :class="{ 'reload-spin': isReloading }">mdi-refresh</v-icon>
22
32
  </v-btn>
23
33
  </v-toolbar>
24
34
 
25
35
  <!-- Desktop Table View -->
26
- <v-data-table
36
+ <v-data-table
27
37
  v-show="!isMobile"
28
- :headers="headers"
29
- :items="filteredTasks"
38
+ :headers="headers"
39
+ :items="filteredTasks"
30
40
  :sort-by="sortBy"
31
- :items-per-page="itemsPerPage"
32
- :items-per-page-options="itemsPerPageOptions"
41
+ :items-per-page="itemsPerPage"
42
+ :items-per-page-options="itemsPerPageOptions"
33
43
  @update:sort-by="updateSortBy"
34
- @update:items-per-page="updateItemsPerPage"
35
- class="full-width-table">
36
-
44
+ @update:items-per-page="updateItemsPerPage"
45
+ class="full-width-table"
46
+ >
37
47
  <template v-slot:item.actions="{ item }">
38
48
  <v-container class="action-button-container">
39
- <v-container v-for="(action, index) in actions"
40
- style="padding: 0px; display: inline; width: fit-content; margin: 0px">
41
- <v-btn style="margin: 0px 2px" class="action-button"
42
- v-if="action && conditionCheck(action.condition, item)" :key="index" size="small"
43
- @click="actionFn(action, item)">
49
+ <v-container
50
+ v-for="(action, index) in actions"
51
+ style="padding: 0px; display: inline; width: fit-content; margin: 0px"
52
+ :key="index"
53
+ >
54
+ <v-btn
55
+ style="margin: 0px 2px"
56
+ class="action-button"
57
+ v-if="action && conditionCheck(action.condition, item)"
58
+ size="small"
59
+ @click="actionFn(action, item)"
60
+ >
44
61
  {{ getActionLabel(action, item) }}
45
62
  </v-btn>
46
63
  </v-container>
@@ -53,12 +70,13 @@
53
70
 
54
71
  <!-- Mobile Cards View -->
55
72
  <div v-show="isMobile" class="mobile-cards-container">
56
- <v-card
57
- v-for="(item, index) in paginatedTasksForMobile"
58
- :key="index"
59
- class="mobile-card ma-2"
73
+ <v-card
74
+ v-for="(item, index) in paginatedTasksForMobile"
75
+ :key="index"
76
+ class="mobile-card ma-2"
60
77
  variant="outlined"
61
- elevation="2">
78
+ elevation="2"
79
+ >
62
80
  <v-card-text class="pb-2">
63
81
  <div v-for="header in dataHeaders" :key="header.key" class="mobile-card-row">
64
82
  <span class="mobile-card-label">{{ header.title }}:</span>
@@ -66,26 +84,27 @@
66
84
  </div>
67
85
  </v-card-text>
68
86
  <v-card-actions v-if="actions && actions.length > 0" class="pt-0 px-2 pb-2 d-flex">
69
- <v-btn
87
+ <v-btn
70
88
  v-for="(action, actionIndex) in visibleActionsForMobile"
71
- :key="actionIndex"
89
+ :key="actionIndex"
72
90
  size="x-small"
73
91
  color="primary"
74
92
  variant="flat"
75
93
  class="flex-grow-1 mx-1"
76
- style="min-height: 28px; height: 28px; font-size: 0.75rem;"
77
- @click="actionFn(action, item)">
94
+ style="min-height: 28px; height: 28px; font-size: 0.75rem"
95
+ @click="actionFn(action, item)"
96
+ >
78
97
  {{ getActionLabel(action, item) }}
79
98
  </v-btn>
80
99
  </v-card-actions>
81
100
  </v-card>
82
-
101
+
83
102
  <!-- No Data Alert for Mobile -->
84
103
  <v-alert v-if="paginatedTasksForMobile.length === 0" text="Keine Daten" class="ma-2"></v-alert>
85
104
  </div>
86
105
 
87
106
  <!-- Pagination for Mobile -->
88
- <v-pagination
107
+ <v-pagination
89
108
  v-if="isMobile && filteredTasks.length > itemsPerPage"
90
109
  v-model="currentPage"
91
110
  :length="Math.ceil(filteredTasks.length / itemsPerPage)"
@@ -96,13 +115,14 @@
96
115
 
97
116
  <script>
98
117
  import { mapState } from 'vuex';
99
- import UIDynamicTableTitleText from './TitleText.vue'
118
+ import UIDynamicTableTitleText from './TitleText.vue';
100
119
  import { debounce } from 'lodash';
120
+ import jsonata from 'jsonata';
101
121
 
102
122
  export default {
103
123
  name: 'UIDynamicTable',
104
124
  components: {
105
- UIDynamicTableTitleText
125
+ UIDynamicTableTitleText,
106
126
  },
107
127
  inject: ['$socket'],
108
128
  props: {
@@ -128,45 +148,49 @@ export default {
128
148
  return this.$vuetify.display.mobile;
129
149
  },
130
150
  dataHeaders() {
131
- return this.headers.filter(header => header.key !== 'actions');
151
+ return this.headers.filter((header) => header.key !== 'actions');
132
152
  },
133
153
  filteredTasks() {
134
154
  let filtered = this.tasks;
135
-
155
+
136
156
  // Apply search filter
137
157
  if (this.search && this.search.trim() !== '') {
138
158
  const searchTerm = this.search.toLowerCase();
139
- filtered = filtered.filter(item => {
140
- return this.dataHeaders.some(header => {
159
+ filtered = filtered.filter((item) => {
160
+ return this.dataHeaders.some((header) => {
141
161
  const value = item[header.key];
142
162
  return value && value.toString().toLowerCase().includes(searchTerm);
143
163
  });
144
164
  });
145
165
  }
146
-
166
+
147
167
  // Don't apply pagination here - let v-data-table handle it for desktop
148
168
  // For mobile, we'll handle pagination separately
149
169
  return filtered;
150
170
  },
151
171
  paginatedTasksForMobile() {
152
172
  if (!this.isMobile) return this.filteredTasks;
153
-
173
+
154
174
  const startIndex = (this.currentPage - 1) * this.itemsPerPage;
155
175
  const endIndex = startIndex + this.itemsPerPage;
156
176
  return this.filteredTasks.slice(startIndex, endIndex);
157
177
  },
158
178
  visibleActionsForMobile() {
159
- return this.actions.filter(action => {
179
+ return this.actions.filter((action) => {
160
180
  if (!action) return false;
161
-
181
+
162
182
  // If no condition or empty condition, show the action
163
183
  if (!action.condition || action.condition === '') return true;
164
-
184
+
165
185
  // For conditions, we'll show all actions and let the click handler deal with it
166
186
  // This avoids the Vue reactivity issue with calling conditionCheck in template
167
187
  return true;
168
188
  });
169
- }
189
+ },
190
+ // Computed property to ensure reactivity to message changes
191
+ currentMessage() {
192
+ return this.messages[this.id] || {};
193
+ },
170
194
  },
171
195
  data() {
172
196
  return {
@@ -185,8 +209,10 @@ export default {
185
209
  { value: 25, title: '25' },
186
210
  { value: 50, title: '50' },
187
211
  { value: 100, title: '100' },
188
- { value: -1, title: 'All' }
212
+ { value: -1, title: 'All' },
189
213
  ],
214
+ // Store JSONata evaluation results for each action-row combination
215
+ jsonataResults: new Map(),
190
216
  };
191
217
  },
192
218
  mounted() {
@@ -233,7 +259,7 @@ export default {
233
259
  return; // Don't execute if condition fails
234
260
  }
235
261
  }
236
-
262
+
237
263
  const msg = this.messages[this.id] || {};
238
264
  msg.payload = item;
239
265
  this.send(
@@ -242,8 +268,29 @@ export default {
242
268
  );
243
269
  },
244
270
  initialize(msg) {
271
+ // Handle null or undefined messages
272
+ if (!msg) {
273
+ this.tasks = [];
274
+ this.actions = (this.props.options || []).filter((action) => action != null);
275
+ this.headers = (this.props.columns || []).map((item) => ({
276
+ title: item.label,
277
+ key: item.value,
278
+ }));
279
+ this.headers.push({
280
+ title: '',
281
+ align: 'center',
282
+ key: 'actions',
283
+ sortable: false,
284
+ });
285
+ // Clear JSONata results cache
286
+ this.jsonataResults.clear();
287
+ return;
288
+ }
289
+
245
290
  this.tasks = msg.payload || [];
246
- this.actions = (this.props.options || []).filter(action => action != null);
291
+
292
+ // Always use props.options for frontend-only evaluation
293
+ this.actions = (this.props.options || []).filter((action) => action != null);
247
294
 
248
295
  this.headers = (this.props.columns || []).map((item) => ({
249
296
  title: item.label,
@@ -254,22 +301,138 @@ export default {
254
301
  title: '',
255
302
  align: 'center',
256
303
  key: 'actions',
257
- sortable: false
304
+ sortable: false,
258
305
  });
306
+
307
+ // Clear JSONata results cache when new data arrives
308
+ this.jsonataResults.clear();
259
309
  },
260
310
  getActionLabel(action, item) {
261
- if (action.label && (action.label.startsWith('msg.') || action.label.startsWith('item.'))) {
311
+ // Handle typed input system
312
+ if (action.label_type) {
313
+ switch (action.label_type) {
314
+ case 'str':
315
+ return action.label || '';
316
+
317
+ case 'msg':
318
+ if (action.label) {
319
+ const msgData = this.messages[this.id] || {};
320
+ return this.getNestedProperty(msgData, action.label) || '';
321
+ }
322
+ return '';
323
+
324
+ case 'row':
325
+ if (action.label) {
326
+ return this.getNestedProperty(item, action.label) || '';
327
+ }
328
+ return '';
329
+
330
+ case 'jsonata':
331
+ // For JSONata, check if we have the expression from backend
332
+ const msgData = this.messages[this.id] || {};
333
+ const actionIndex = this.actions.indexOf(action);
334
+
335
+ if (msgData._jsonataLabels && msgData._jsonataLabels[actionIndex]) {
336
+ const result = this.evaluateJsonata(msgData._jsonataLabels[actionIndex], msgData, item);
337
+ return result || '';
338
+ }
339
+ return action.label || '';
340
+
341
+ default:
342
+ return action.label || '';
343
+ }
344
+ }
345
+
346
+ // Fallback to legacy handling for backward compatibility
347
+ const legacy = this.evaluateLegacyLabel(action, item);
348
+ return legacy || action.label || '';
349
+ },
350
+
351
+ getNestedProperty(obj, path) {
352
+ if (!path) return '';
353
+
354
+ const keys = path.split('.');
355
+ let current = obj;
356
+
357
+ for (const key of keys) {
358
+ if (current && typeof current === 'object' && key in current) {
359
+ current = current[key];
360
+ } else {
361
+ return path; // Return original path as fallback
362
+ }
363
+ }
364
+
365
+ return current !== undefined && current !== null ? String(current) : path;
366
+ },
367
+
368
+ evaluateJsonata(expression, msgData, rowData) {
369
+ try {
370
+ // Create a unique key for this evaluation using expression and a hash of row data
371
+ const rowHash = JSON.stringify(rowData).slice(0, 50); // Truncate for performance
372
+ const key = `${expression}_${rowHash}`;
373
+
374
+ // Check if we already have a result for this combination
375
+ if (this.jsonataResults.has(key)) {
376
+ return this.jsonataResults.get(key);
377
+ }
378
+
379
+ // Create a combined context with both message and row data
380
+ const context = {
381
+ ...msgData,
382
+ $: rowData,
383
+ payload: msgData.payload,
384
+ msg: msgData,
385
+ };
386
+
387
+ const expr = jsonata(expression);
388
+ const result = expr.evaluate(context);
389
+
390
+ // Handle Promise return from JSONata
391
+ if (result && typeof result.then === 'function') {
392
+ // Store a pending indicator first
393
+ this.jsonataResults.set(key, expression);
394
+
395
+ // Handle the Promise
396
+ result
397
+ .then((resolvedValue) => {
398
+ const finalValue =
399
+ resolvedValue !== undefined && resolvedValue !== null ? String(resolvedValue) : '';
400
+ this.jsonataResults.set(key, finalValue);
401
+ this.$forceUpdate();
402
+ })
403
+ .catch((error) => {
404
+ console.warn('JSONata Promise evaluation error:', error.message);
405
+ this.jsonataResults.set(key, expression);
406
+ this.$forceUpdate();
407
+ });
408
+
409
+ return expression; // Return expression as placeholder
410
+ }
411
+
412
+ // Synchronous result
413
+ const finalValue = result !== undefined && result !== null ? String(result) : '';
414
+ this.jsonataResults.set(key, finalValue);
415
+ return finalValue;
416
+ } catch (error) {
417
+ console.warn('JSONata evaluation error:', error.message);
418
+ return expression; // fallback to original expression
419
+ }
420
+ },
421
+
422
+ evaluateLegacyLabel(action, item) {
423
+ // Legacy support for old "msg." and "row." prefixes
424
+ if (action.label && (action.label.startsWith('msg.') || action.label.startsWith('row.'))) {
262
425
  let sourceObject;
263
426
  let path;
264
-
427
+
265
428
  if (action.label.startsWith('msg.')) {
266
429
  sourceObject = this.messages[this.id] || {};
267
430
  path = action.label.substring(4).split('.');
268
- } else if (action.label.startsWith('item.')) {
431
+ } else if (action.label.startsWith('row.')) {
269
432
  sourceObject = item;
270
- path = action.label.substring(5).split('.');
433
+ path = action.label.substring(4).split('.');
271
434
  }
272
-
435
+
273
436
  // Navigate through the object using the path
274
437
  let value = sourceObject;
275
438
  for (const key of path) {
@@ -280,10 +443,11 @@ export default {
280
443
  return action.label;
281
444
  }
282
445
  }
283
-
446
+
284
447
  return value !== undefined && value !== null ? String(value) : action.label;
285
448
  }
286
- return action.label;
449
+
450
+ return action.label || '';
287
451
  },
288
452
  conditionCheck(condition, row) {
289
453
  if (!condition || condition === '') return true;
@@ -298,15 +462,18 @@ export default {
298
462
  reloadData() {
299
463
  this.isReloading = true;
300
464
 
301
- this.send({
302
- _client: {
303
- socketId: this.$socket.id,
304
- widgetId: this.id,
305
- },
306
- payload: {
307
- action: 'reload',
465
+ this.send(
466
+ {
467
+ _client: {
468
+ socketId: this.$socket.id,
469
+ widgetId: this.id,
470
+ },
471
+ payload: {
472
+ action: 'reload',
473
+ },
308
474
  },
309
- }, 0);
475
+ 0
476
+ );
310
477
 
311
478
  setTimeout(() => {
312
479
  this.isReloading = false;
@@ -321,14 +488,16 @@ export default {
321
488
  delete currentQuery[this.searchParamKey];
322
489
  }
323
490
 
324
- this.$router.replace({
325
- path: this.$route.path,
326
- query: currentQuery
327
- }).catch(err => {
328
- if (err.name !== 'NavigationDuplicated') {
329
- console.error('Router navigation error:', err);
330
- }
331
- });
491
+ this.$router
492
+ .replace({
493
+ path: this.$route.path,
494
+ query: currentQuery,
495
+ })
496
+ .catch((err) => {
497
+ if (err.name !== 'NavigationDuplicated') {
498
+ console.error('Router navigation error:', err);
499
+ }
500
+ });
332
501
  }, 500),
333
502
  loadSearchFromUrl() {
334
503
  const searchValue = this.$route.query[this.searchParamKey];
@@ -382,8 +551,8 @@ export default {
382
551
  this.loadSearchFromUrl();
383
552
  }
384
553
  },
385
- deep: true
386
- }
554
+ deep: true,
555
+ },
387
556
  },
388
557
  };
389
558
  </script>
@@ -443,20 +612,20 @@ export default {
443
612
  .mobile-cards-container {
444
613
  padding: 4px;
445
614
  }
446
-
615
+
447
616
  .mobile-card {
448
617
  margin-bottom: 8px !important;
449
618
  }
450
-
619
+
451
620
  .mobile-card-label {
452
621
  flex: 0 0 45%;
453
622
  font-size: 0.8rem;
454
623
  }
455
-
624
+
456
625
  .mobile-card-value {
457
626
  font-size: 0.8rem;
458
627
  }
459
-
628
+
460
629
  .search-input {
461
630
  margin-left: 8px;
462
631
  margin-right: 8px;