@5minds/node-red-dashboard-2-processcube-dynamic-form 1.0.25-feature-1a3e84-m2en1jfr → 1.0.25-poc-for-using-dynamic-ui-from-app-sdk-4a73f4-m2ok3vom

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.
@@ -0,0 +1,688 @@
1
+ <template>
2
+ <!-- Component must be wrapped in a block so props such as className and style can be passed in from parent -->
3
+ <div className="ui-dynamic-form-wrapper">
4
+ <p v-if="hasFields()">
5
+ <v-form ref="form" v-model="form" :class="dynamicClass">
6
+ <h3 style="padding: 16px">{{ this.props.name }}</h3>
7
+ <div style="padding: 16px; max-height: 550px; overflow-y: auto">
8
+ <v-row v-for="(field, index) in fields()" :key="index">
9
+ <v-col cols="12">
10
+ <component
11
+ v-if="createComponent(field).innerText"
12
+ :is="createComponent(field).type"
13
+ v-bind="createComponent(field).props"
14
+ v-model="formData[field.id]"
15
+ >
16
+ {{ createComponent(field).innerText }}
17
+ </component>
18
+ <div v-else-if="createComponent(field).type == 'v-slider'">
19
+ <p class="formkit-label">{{ field.label }}</p>
20
+ <component
21
+ :is="createComponent(field).type"
22
+ v-bind="createComponent(field).props"
23
+ v-model="field.defaultValue"
24
+ />
25
+ <p class="formkit-help">
26
+ {{ field.customForm ? JSON.parse(field.customForm).hint : undefined }}
27
+ </p>
28
+ </div>
29
+ <component
30
+ v-else
31
+ :is="createComponent(field).type"
32
+ v-bind="createComponent(field).props"
33
+ v-model="formData[field.id]"
34
+ />
35
+ </v-col>
36
+ </v-row>
37
+ </div>
38
+ <v-row :class="dynamicFooterClass">
39
+ <v-row v-if="error" style="padding: 12px">
40
+ <v-alert v-if="error" type="error">Error: {{ errorMsg }}</v-alert>
41
+ </v-row>
42
+ <div style="display: flex; gap: 8px">
43
+ <div v-for="(action, index) in actions" :key="index" style="flex-grow: 1">
44
+ <v-btn
45
+ :key="index"
46
+ style="width: 100% !important; min-height: 36px"
47
+ @click="actionFn(action)"
48
+ >
49
+ {{ action.label }}
50
+ </v-btn>
51
+ </div>
52
+ </div>
53
+ </v-row>
54
+ </v-form>
55
+ </p>
56
+ <p v-else>
57
+ <v-alert :text="waiting_info" :title="waiting_title" />
58
+ </p>
59
+ </div>
60
+ </template>
61
+
62
+ <script>
63
+ import { markRaw, h } from 'vue';
64
+ import { mapState } from 'vuex';
65
+ import { plugin, defaultConfig } from '@formkit/vue';
66
+ import '@formkit/themes/genesis';
67
+ import { FormKit } from '@formkit/vue';
68
+
69
+ export default {
70
+ name: 'UIDynamicForm',
71
+ inject: ['$socket'],
72
+ props: {
73
+ /* do not remove entries from this - Dashboard's Layout Manager's will pass this data to your component */
74
+ id: { type: String, required: true },
75
+ props: { type: Object, default: () => ({}) },
76
+ state: {
77
+ type: Object,
78
+ default: () => ({ enabled: false, visible: false }),
79
+ },
80
+ },
81
+ setup(props) {
82
+ console.info('UIDynamicForm setup with:', props);
83
+ console.debug('Vue function loaded correctly', markRaw);
84
+ },
85
+ data() {
86
+ return {
87
+ actions: [],
88
+ form: {},
89
+ formData: {},
90
+ taskInput: {},
91
+ theme: '',
92
+ error: false,
93
+ errorMsg: '',
94
+ };
95
+ },
96
+ created() {
97
+ const currentPath = window.location.pathname;
98
+ const lastPart = currentPath.substring(currentPath.lastIndexOf('/'));
99
+
100
+ const store = this.$store.state;
101
+
102
+ for (let key in store.ui.pages) {
103
+ if (store.ui.pages[key].path === lastPart) {
104
+ const theme = store.ui.pages[key].theme;
105
+ if (store.ui.themes[theme].name === 'ProcessCube Lightmode') {
106
+ this.theme = 'light';
107
+ } else if (store.ui.themes[theme].name === 'ProcessCube Darkmode') {
108
+ this.theme = 'dark';
109
+ } else {
110
+ this.theme = 'default';
111
+ }
112
+ break;
113
+ }
114
+ }
115
+
116
+ const formkitConfig = defaultConfig({
117
+ theme: 'genesis',
118
+ });
119
+ window.app.use(plugin, formkitConfig);
120
+ },
121
+ computed: {
122
+ ...mapState('data', ['messages']),
123
+ waiting_title() {
124
+ return this.props.waiting_title || 'Warten auf den Usertask...';
125
+ },
126
+ waiting_info() {
127
+ return (
128
+ this.props.waiting_info ||
129
+ 'Der Usertask wird automatisch angezeigt, wenn ein entsprechender Task vorhanden ist.'
130
+ );
131
+ },
132
+
133
+ dynamicClass() {
134
+ return `ui-dynamic-form-${this.theme}`;
135
+ },
136
+
137
+ dynamicFooterClass() {
138
+ return `ui-dynamic-form-footer-${this.theme}`;
139
+ },
140
+ },
141
+ mounted() {
142
+ const elements = document.querySelectorAll('.formkit-input');
143
+
144
+ elements.forEach((element) => {
145
+ element.classList.add('test');
146
+ });
147
+
148
+ this.$socket.on('widget-load:' + this.id, (msg) => {
149
+ this.init();
150
+ this.$store.commit('data/bind', {
151
+ widgetId: this.id,
152
+ msg,
153
+ });
154
+ });
155
+ this.$socket.on('msg-input:' + this.id, (msg) => {
156
+ // store the latest message in our client-side vuex store when we receive a new message
157
+ this.init();
158
+
159
+ this.messages[this.id] = msg;
160
+
161
+ const hasTask = msg.payload && msg.payload.userTask;
162
+ const defaultValues = msg.payload.userTask.userTaskConfig.formFields;
163
+ const initialValues = msg.payload.userTask.startToken;
164
+
165
+ if (hasTask) {
166
+ this.taskInput = msg.payload.userTask;
167
+ }
168
+
169
+ if (hasTask && defaultValues) {
170
+ defaultValues.forEach((field) => {
171
+ this.formData[field.id] = field.defaultValue;
172
+ });
173
+ }
174
+
175
+ if (hasTask && initialValues) {
176
+ Object.keys(initialValues).forEach((key) => {
177
+ this.formData[key] = initialValues[key];
178
+ });
179
+ }
180
+
181
+ this.$store.commit('data/bind', {
182
+ widgetId: this.id,
183
+ msg,
184
+ });
185
+ });
186
+ // tell Node-RED that we're loading a new instance of this widget
187
+ this.$socket.emit('widget-load', this.id);
188
+ },
189
+ unmounted() {
190
+ /* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
191
+ this.$socket?.off('widget-load' + this.id);
192
+ this.$socket?.off('msg-input:' + this.id);
193
+ },
194
+ components: {
195
+ FormKit,
196
+ },
197
+ methods: {
198
+ createComponent(field) {
199
+ const hint = field.customForm ? JSON.parse(field.customForm).hint : undefined;
200
+ const placeholder = field.customForm ? JSON.parse(field.customForm).placeholder : undefined;
201
+ switch (field.type) {
202
+ case 'long':
203
+ return {
204
+ type: 'FormKit',
205
+ props: {
206
+ type: 'number',
207
+ id: field.id,
208
+ label: field.label,
209
+ required: field.required,
210
+ value: field.defaultValue,
211
+ number: 'integer',
212
+ help: hint,
213
+ wrapperClass: '$remove:formkit-wrapper',
214
+ inputClass: `input-${this.theme}`,
215
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
216
+ },
217
+ };
218
+ case 'number':
219
+ const step = field.customForm ? JSON.parse(field.customForm).step : undefined;
220
+ return {
221
+ type: 'FormKit',
222
+ props: {
223
+ type: 'number',
224
+ id: field.id,
225
+ label: field.label,
226
+ required: field.required,
227
+ value: field.defaultValue,
228
+ step: step,
229
+ help: hint,
230
+ wrapperClass: '$remove:formkit-wrapper',
231
+ inputClass: `input-${this.theme}`,
232
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
233
+ },
234
+ };
235
+ case 'date':
236
+ return {
237
+ type: 'FormKit',
238
+ props: {
239
+ type: 'date',
240
+ id: field.id,
241
+ label: field.label,
242
+ required: field.required,
243
+ value: field.defaultValue,
244
+ help: hint,
245
+ wrapperClass: '$remove:formkit-wrapper',
246
+ inputClass: `input-${this.theme}`,
247
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
248
+ },
249
+ };
250
+ case 'enum':
251
+ const enums = field.enumValues.map((obj) => {
252
+ return { value: obj.id, label: obj.name };
253
+ });
254
+ return {
255
+ type: 'FormKit',
256
+ props: {
257
+ type: 'select', // JSON.parse(field.customForm).displayAs
258
+ id: field.id,
259
+ label: field.label,
260
+ required: field.required,
261
+ value: field.defaultValue,
262
+ options: enums,
263
+ help: hint,
264
+ wrapperClass: '$remove:formkit-wrapper',
265
+ inputClass: `input-${this.theme}`,
266
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
267
+ },
268
+ };
269
+ case 'select':
270
+ const selections = JSON.parse(field.customForm).entries.map((obj) => {
271
+ return { value: obj.key, label: obj.value };
272
+ });
273
+ return {
274
+ type: 'FormKit',
275
+ props: {
276
+ type: 'select', // JSON.parse(field.customForm).displayAs
277
+ id: field.id,
278
+ label: field.label,
279
+ required: field.required,
280
+ value: field.defaultValue,
281
+ options: selections,
282
+ placeholder: placeholder,
283
+ help: hint,
284
+ wrapperClass: '$remove:formkit-wrapper',
285
+ inputClass: `input-${this.theme}`,
286
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
287
+ },
288
+ };
289
+ case 'string':
290
+ return {
291
+ type: 'FormKit',
292
+ props: {
293
+ type: 'text',
294
+ id: field.id,
295
+ label: field.label,
296
+ required: field.required,
297
+ value: field.defaultValue,
298
+ help: hint,
299
+ placeholder: placeholder,
300
+ wrapperClass: '$remove:formkit-wrapper',
301
+ inputClass: `input-${this.theme}`,
302
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
303
+ },
304
+ };
305
+ case 'boolean':
306
+ return {
307
+ type: 'FormKit',
308
+ props: {
309
+ type: 'checkbox',
310
+ id: field.id,
311
+ label: field.label,
312
+ required: field.required,
313
+ value: field.defaultValue,
314
+ help: hint,
315
+ inputClass: `input-${this.theme}`,
316
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
317
+ },
318
+ };
319
+ case 'file':
320
+ return {
321
+ type: 'FormKit',
322
+ props: {
323
+ type: 'file',
324
+ id: field.id,
325
+ label: field.label,
326
+ required: field.required,
327
+ value: field.defaultValue,
328
+ help: hint,
329
+ innerClass: 'reset-background',
330
+ wrapperClass: '$remove:formkit-wrapper',
331
+ inputClass: `input-${this.theme}`,
332
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
333
+ },
334
+ };
335
+ case 'checkbox':
336
+ const options = JSON.parse(field.customForm).entries.map((obj) => {
337
+ return { value: obj.key, label: obj.value };
338
+ });
339
+ return {
340
+ type: 'FormKit',
341
+ props: {
342
+ type: 'checkbox',
343
+ id: field.id,
344
+ label: field.label,
345
+ required: field.required,
346
+ value: field.defaultValue,
347
+ options: options,
348
+ help: hint,
349
+ fieldsetClass: 'custom-fieldset',
350
+ inputClass: `input-${this.theme}`,
351
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
352
+ },
353
+ };
354
+ case 'color':
355
+ return {
356
+ type: 'FormKit',
357
+ props: {
358
+ type: 'color',
359
+ id: field.id,
360
+ label: field.label,
361
+ required: field.required,
362
+ value: field.defaultValue,
363
+ help: hint,
364
+ },
365
+ };
366
+ case 'datetime-local':
367
+ return {
368
+ type: 'FormKit',
369
+ props: {
370
+ type: 'datetime-local',
371
+ id: field.id,
372
+ label: field.label,
373
+ required: field.required,
374
+ value: field.defaultValue,
375
+ help: hint,
376
+ wrapperClass: '$remove:formkit-wrapper',
377
+ inputClass: `input-${this.theme}`,
378
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
379
+ },
380
+ };
381
+ case 'email':
382
+ return {
383
+ type: 'FormKit',
384
+ props: {
385
+ type: 'email',
386
+ id: field.id,
387
+ label: field.label,
388
+ required: field.required,
389
+ value: field.defaultValue,
390
+ help: hint,
391
+ validation: 'email',
392
+ validationVisibility: 'live',
393
+ placeholder: placeholder,
394
+ wrapperClass: '$remove:formkit-wrapper',
395
+ inputClass: `input-${this.theme}`,
396
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
397
+ },
398
+ };
399
+ case 'header':
400
+ let typeToUse = 'h1';
401
+ if (field.customForm && JSON.parse(field.customForm).style == 'heading_2') {
402
+ typeToUse = 'h2';
403
+ }
404
+ if (field.customForm && JSON.parse(field.customForm).style == 'heading_3') {
405
+ typeToUse = 'h3';
406
+ }
407
+ return {
408
+ type: typeToUse,
409
+ innerText: field.defaultValue,
410
+ };
411
+ case 'hidden':
412
+ return {
413
+ type: 'input',
414
+ props: {
415
+ type: 'hidden',
416
+ value: field.defaultValue,
417
+ },
418
+ };
419
+ case 'month':
420
+ return {
421
+ type: 'FormKit',
422
+ props: {
423
+ type: 'month',
424
+ id: field.id,
425
+ label: field.label,
426
+ required: field.required,
427
+ value: field.defaultValue,
428
+ help: hint,
429
+ wrapperClass: '$remove:formkit-wrapper',
430
+ inputClass: `input-${this.theme}`,
431
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
432
+ },
433
+ };
434
+ case 'paragraph':
435
+ return {
436
+ type: 'p',
437
+ innerText: field.defaultValue,
438
+ };
439
+ case 'password':
440
+ return {
441
+ type: 'FormKit',
442
+ props: {
443
+ type: 'password',
444
+ id: field.id,
445
+ label: field.label,
446
+ required: field.required,
447
+ value: field.defaultValue,
448
+ help: hint,
449
+ placeholder: placeholder,
450
+ wrapperClass: '$remove:formkit-wrapper',
451
+ inputClass: `input-${this.theme}`,
452
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
453
+ },
454
+ };
455
+ case 'radio':
456
+ const radioOptions = JSON.parse(field.customForm).entries.map((obj) => {
457
+ return { value: obj.key, label: obj.value };
458
+ });
459
+ return {
460
+ type: 'FormKit',
461
+ props: {
462
+ type: 'radio',
463
+ id: field.id,
464
+ label: field.label,
465
+ required: field.required,
466
+ value: field.defaultValue,
467
+ options: radioOptions,
468
+ help: hint,
469
+ fieldsetClass: 'custom-fieldset',
470
+ inputClass: `input-${this.theme}`,
471
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
472
+ },
473
+ };
474
+ case 'range':
475
+ const customForm = JSON.parse(field.customForm);
476
+ return {
477
+ type: 'v-slider',
478
+ props: {
479
+ id: field.id,
480
+ // label: field.label,
481
+ required: field.required,
482
+ // value: field.defaultValue,
483
+ // help: hint,
484
+ min: customForm.min,
485
+ max: customForm.max,
486
+ step: customForm.step,
487
+ thumbLabel: true,
488
+ // wrapperClass: '$remove:formkit-wrapper',
489
+ // inputClass: `input-${this.theme}`,
490
+ // innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
491
+ },
492
+ };
493
+ case 'tel':
494
+ return {
495
+ type: 'FormKit',
496
+ props: {
497
+ type: 'tel' /* with pro component mask more good */,
498
+ id: field.id,
499
+ label: field.label,
500
+ required: field.required,
501
+ value: field.defaultValue,
502
+ help: hint,
503
+ placeholder: placeholder,
504
+ wrapperClass: '$remove:formkit-wrapper',
505
+ inputClass: `input-${this.theme}`,
506
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
507
+ },
508
+ };
509
+ case 'textarea':
510
+ const rows = field.customForm ? JSON.parse(field.customForm).rows : undefined;
511
+ return {
512
+ type: 'FormKit',
513
+ props: {
514
+ type: 'textarea' /* with pro component mask more good */,
515
+ id: field.id,
516
+ label: field.label,
517
+ required: field.required,
518
+ value: field.defaultValue,
519
+ rows: rows,
520
+ help: hint,
521
+ placeholder: placeholder,
522
+ wrapperClass: '$remove:formkit-wrapper',
523
+ inputClass: `input-${this.theme}`,
524
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
525
+ },
526
+ };
527
+ case 'time':
528
+ return {
529
+ type: 'FormKit',
530
+ props: {
531
+ type: 'time' /* with pro component mask more good */,
532
+ id: field.id,
533
+ label: field.label,
534
+ required: field.required,
535
+ value: field.defaultValue,
536
+ help: hint,
537
+ placeholder: placeholder,
538
+ wrapperClass: '$remove:formkit-wrapper',
539
+ inputClass: `input-${this.theme}`,
540
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
541
+ },
542
+ };
543
+ case 'url':
544
+ return {
545
+ type: 'FormKit',
546
+ props: {
547
+ type: 'url',
548
+ id: field.id,
549
+ label: field.label,
550
+ required: field.required,
551
+ value: field.defaultValue,
552
+ help: hint,
553
+ placeholder: placeholder,
554
+ validation: 'url',
555
+ validationVisibility: 'live',
556
+ wrapperClass: '$remove:formkit-wrapper',
557
+ inputClass: `input-${this.theme}`,
558
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
559
+ },
560
+ };
561
+ case 'week':
562
+ return {
563
+ type: 'FormKit',
564
+ props: {
565
+ type: 'week',
566
+ id: field.id,
567
+ label: field.label,
568
+ required: field.required,
569
+ value: field.defaultValue,
570
+ help: hint,
571
+ placeholder: placeholder,
572
+ wrapperClass: '$remove:formkit-wrapper',
573
+ inputClass: `input-${this.theme}`,
574
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
575
+ },
576
+ };
577
+ default:
578
+ return {
579
+ type: 'FormKit',
580
+ props: {
581
+ type: field.type,
582
+ id: field.id,
583
+ label: field.label,
584
+ required: field.required,
585
+ value: field.defaultValue,
586
+ help: hint,
587
+ inputClass: `input-${this.theme}`,
588
+ innerClass: `${this.theme == 'dark' ? '$remove:formkit-inner' : ''}`,
589
+ },
590
+ };
591
+ }
592
+ },
593
+ checkFormState(state) {
594
+ // const field = this.$formkit.get('field_01');
595
+ // console.info(field.context.state.valid);
596
+
597
+ return true;
598
+
599
+ // loop over fields then this.$formkit.get(this.id) -> check error state if all ok return true else return false
600
+ // ?? wie unterscheiden wir welche actions dieser validierungsfehler betrifft ??
601
+ // ?? wie machen wir formkit validierung auch im Studio available ??
602
+ // \_ vllt macht es sinn das schema von formkit zu übernehmen oder alternativ nur unsere validierung zu nutzen.
603
+ },
604
+ hasUserTask() {
605
+ return this.messages && this.messages[this.id] && this.messages[this.id].payload.userTask;
606
+ },
607
+ userTask() {
608
+ return this.hasUserTask() ? this.messages[this.id].payload.userTask : {};
609
+ },
610
+ fields() {
611
+ const aFields = this.hasUserTask() ? this.userTask().userTaskConfig.formFields : [];
612
+ const fieldMap = aFields.map((field) => ({
613
+ ...field,
614
+ items: mapItems(field.type, field),
615
+ }));
616
+
617
+ return fieldMap;
618
+ },
619
+ hasFields() {
620
+ return this.messages && this.messages[this.id] && this.messages[this.id].payload.userTask !== undefined;
621
+ },
622
+ /*
623
+ widget-action just sends a msg to Node-RED, it does not store the msg state server-side
624
+ alternatively, you can use widget-change, which will also store the msg in the Node's datastore
625
+ */
626
+ send(msg, index) {
627
+ const msgArr = [];
628
+ msgArr[index] = msg;
629
+ this.$socket.emit('widget-action', this.id, msgArr);
630
+ },
631
+ init() {
632
+ this.actions = this.props.options;
633
+ },
634
+ actionFn(action) {
635
+ // this.checkFormState();
636
+ if (this.checkCondition(action.condition)) {
637
+ this.showError(false, '');
638
+ // TODO: MM - begin
639
+ // this.send(
640
+ // { payload: { formData: this.formData, userTask: this.userTask() } },
641
+ // this.actions.findIndex((element) => element.label === action.label)
642
+ // );
643
+ const msg = this.messages[this.id] || {};
644
+ msg.payload = { formData: this.formData, userTask: this.userTask() };
645
+ this.send(
646
+ msg,
647
+ this.actions.findIndex((element) => element.label === action.label)
648
+ );
649
+ // TODO: mm - end
650
+ } else {
651
+ this.showError(true, action.errorMessage);
652
+ }
653
+ },
654
+ checkCondition(condition) {
655
+ if (condition == '') return true;
656
+ try {
657
+ const func = Function('fields', 'userTask', 'msg', '"use strict"; return (' + condition + ')');
658
+ const result = func(this.formData, this.taskInput, this.messages[this.id]);
659
+ console.log(this.formData, result);
660
+ return Boolean(result);
661
+ } catch (err) {
662
+ console.error('Error while evaluating condition: ' + err);
663
+ return false;
664
+ }
665
+ },
666
+ showError(bool, errMsg) {
667
+ this.error = bool;
668
+ this.errorMsg = errMsg;
669
+ },
670
+ },
671
+ };
672
+
673
+ function mapItems(type, field) {
674
+ if (type === 'enum') {
675
+ return field.enumValues.map((enumValue) => ({
676
+ title: enumValue.name,
677
+ value: enumValue.id,
678
+ }));
679
+ } else {
680
+ return null;
681
+ }
682
+ }
683
+ </script>
684
+
685
+ <style>
686
+ /* CSS is auto scoped, but using named classes is still recommended */
687
+ @import '../stylesheets/ui-dynamic-form.css';
688
+ </style>