@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0 → 2.1.1-develop-1e7b10-me1d8ayv

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,85 +1,127 @@
1
1
  <template>
2
- <div className="ui-dynamic-form-external-sizing-wrapper" :style="props.card_size_styling">
3
- <!-- Component must be wrapped in a block so props such as className and style can be passed in from parent -->
4
- <UIDynamicFormTitleText v-if="props.title_style === 'outside' && hasUserTask" :style="props.title_style"
5
- :title="effectiveTitle" :customStyles="props.title_custom_text_styling" :titleIcon="props.title_icon"
6
- :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)" :collapsed="collapsed"
7
- :toggleCollapse="toggleCollapse" />
8
- <div className="ui-dynamic-form-wrapper">
9
- <p v-if="hasUserTask" style="margin-bottom: 0px">
10
- <v-form ref="form" v-model="form" :class="dynamicClass">
11
- <UIDynamicFormTitleText v-if="props.title_style != 'outside'" :style="props.title_style"
12
- :title="effectiveTitle" :customStyles="props.title_custom_text_styling"
13
- :titleIcon="props.title_icon"
14
- :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
15
- :collapsed="collapsed" :toggleCollapse="toggleCollapse" />
16
- <Transition name="cardCollapse">
17
- <div v-if="!collapsed">
18
- <div className="ui-dynamic-form-formfield-positioner" :style="props.inner_card_styling"
19
- :data-columns="props.form_columns || 1">
20
- <FormKit id="form" type="group">
21
- <v-row v-for="(field, index) in fields()" :key="field"
22
- :class="field.type === 'header' ? 'ui-dynamic-form-header-row' : ''"
23
- :style="getRowWidthStyling(field, index)">
24
- <v-col cols="12">
25
- <component :is="createComponent(field).type"
26
- v-if="createComponent(field).innerHTML"
27
- v-bind="createComponent(field).props"
28
- :class="createComponent(field).class"
29
- v-html="createComponent(field).innerHTML" :ref="(el) => {
30
- if (index === 0) firstFormFieldRef = el;
31
- }
32
- " />
33
- <component :is="createComponent(field).type"
34
- v-else-if="createComponent(field).innerText"
35
- v-bind="createComponent(field).props" :ref="(el) => {
36
- if (index === 0) firstFormFieldRef = el;
37
- }
38
- " v-model="formData[field.id]">
39
- {{ createComponent(field).innerText }}
40
- </component>
41
- <div v-else-if="createComponent(field).type == 'v-slider'">
42
- <p class="formkit-label">{{ field.label }}</p>
43
- <component :is="createComponent(field).type"
44
- v-bind="createComponent(field).props" :ref="(el) => {
45
- if (index === 0) firstFormFieldRef = el;
46
- }
47
- " v-model="field.defaultValue" />
48
- <p class="formkit-help">
49
- {{ field.customForm ? JSON.parse(field.customForm).hint : undefined
50
- }}
51
- </p>
52
- </div>
53
- <component :is="createComponent(field).type" v-else
54
- v-bind="createComponent(field).props" :ref="(el) => {
55
- if (index === 0) firstFormFieldRef = el;
56
- }
57
- " v-model="formData[field.id]" />
58
- </v-col>
59
- </v-row>
60
- </FormKit>
61
- </div>
62
- <v-row :class="dynamicFooterClass">
63
- <v-row v-if="errorMsg.length > 0" style="padding: 12px">
64
- <v-alert type="error">Error: {{ errorMsg }}</v-alert>
65
- </v-row>
66
- <UIDynamicFormFooterAction v-if="props.actions_inside_card && actions.length > 0"
67
- :actions="actions" :actionCallback="actionFn" :formIsFinished="formIsFinished"
68
- style="padding: 16px; padding-top: 0px" />
69
- </v-row>
70
- </div>
71
- </Transition>
72
- </v-form>
73
- </p>
74
- <p v-else>
75
- <v-alert v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
76
- :text="props.waiting_info" :title="props.waiting_title" />
77
- </p>
78
- </div>
79
- <div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px">
80
- <UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
81
- </div>
2
+ <div className="ui-dynamic-form-external-sizing-wrapper" :style="props.card_size_styling">
3
+ <UIDynamicFormTitleText
4
+ v-if="props.title_style === 'outside' && hasUserTask"
5
+ :style="props.title_style"
6
+ :title="effectiveTitle"
7
+ :customStyles="props.title_custom_text_styling"
8
+ :titleIcon="props.title_icon"
9
+ :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
10
+ :collapsed="collapsed"
11
+ :toggleCollapse="toggleCollapse"
12
+ />
13
+ <div className="ui-dynamic-form-wrapper">
14
+ <p v-if="hasUserTask" style="margin-bottom: 0px">
15
+ <v-form ref="form" v-model="form" :class="dynamicClass">
16
+ <UIDynamicFormTitleText
17
+ v-if="props.title_style != 'outside'"
18
+ :style="props.title_style"
19
+ :title="effectiveTitle"
20
+ :customStyles="props.title_custom_text_styling"
21
+ :titleIcon="props.title_icon"
22
+ :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
23
+ :collapsed="collapsed"
24
+ :toggleCollapse="toggleCollapse"
25
+ />
26
+ <Transition name="cardCollapse">
27
+ <div v-if="!collapsed">
28
+ <div
29
+ className="ui-dynamic-form-formfield-positioner"
30
+ :style="props.inner_card_styling"
31
+ :data-columns="props.form_columns || 1"
32
+ >
33
+ <FormKit id="form" type="group">
34
+ <v-row
35
+ v-for="(field, index) in fields()"
36
+ :key="field"
37
+ :class="field.type === 'header' ? 'ui-dynamic-form-header-row' : ''"
38
+ :style="getRowWidthStyling(field, index)"
39
+ >
40
+ <v-col cols="12">
41
+ <component
42
+ :is="createComponent(field).type"
43
+ v-if="createComponent(field).innerHTML"
44
+ v-bind="createComponent(field).props"
45
+ :class="createComponent(field).class"
46
+ v-html="createComponent(field).innerHTML"
47
+ :ref="
48
+ (el) => {
49
+ if (index === 0) firstFormFieldRef = el;
50
+ }
51
+ "
52
+ />
53
+ <component
54
+ :is="createComponent(field).type"
55
+ v-else-if="createComponent(field).innerText"
56
+ v-bind="createComponent(field).props"
57
+ :ref="
58
+ (el) => {
59
+ if (index === 0) firstFormFieldRef = el;
60
+ }
61
+ "
62
+ v-model="formData[field.id]"
63
+ >
64
+ {{ createComponent(field).innerText }}
65
+ </component>
66
+ <div v-else-if="createComponent(field).type == 'v-slider'">
67
+ <p class="formkit-label">{{ field.label }}</p>
68
+ <component
69
+ :is="createComponent(field).type"
70
+ v-bind="createComponent(field).props"
71
+ :ref="
72
+ (el) => {
73
+ if (index === 0) firstFormFieldRef = el;
74
+ }
75
+ "
76
+ v-model="field.defaultValue"
77
+ />
78
+ <p class="formkit-help">
79
+ {{ field.customForm ? field.customForm.hint : undefined }}
80
+ </p>
81
+ </div>
82
+ <component
83
+ :is="createComponent(field).type"
84
+ v-else
85
+ v-bind="createComponent(field).props"
86
+ :ref="
87
+ (el) => {
88
+ if (index === 0) firstFormFieldRef = el;
89
+ }
90
+ "
91
+ v-model="formData[field.id]"
92
+ />
93
+ </v-col>
94
+ </v-row>
95
+ </FormKit>
96
+ </div>
97
+ <v-row :class="dynamicFooterClass">
98
+ <v-row v-if="errorMsg.length > 0" style="padding: 12px">
99
+ <v-alert type="error">Error: {{ errorMsg }}</v-alert>
100
+ </v-row>
101
+ <UIDynamicFormFooterAction
102
+ v-if="props.actions_inside_card && hasUserTask && actions.length > 0"
103
+ :actions="actions"
104
+ :actionCallback="actionFn"
105
+ :formIsFinished="formIsFinished"
106
+ style="padding: 16px; padding-top: 0px"
107
+ />
108
+ </v-row>
109
+ </div>
110
+ </Transition>
111
+ </v-form>
112
+ </p>
113
+ <p v-else>
114
+ <v-alert
115
+ v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
116
+ :text="props.waiting_info"
117
+ :title="props.waiting_title"
118
+ />
119
+ </p>
120
+ </div>
121
+ <div v-if="!props.actions_inside_card && hasUserTask && actions.length > 0" style="padding-top: 32px">
122
+ <UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
82
123
  </div>
124
+ </div>
83
125
  </template>
84
126
 
85
127
  <script>
@@ -94,938 +136,968 @@ import UIDynamicFormFooterAction from './FooterActions.vue';
94
136
  import UIDynamicFormTitleText from './TitleText.vue';
95
137
 
96
138
  function requiredIf({ value }, [targetField, expectedValue], node) {
97
- console.debug(arguments);
139
+ console.debug(arguments);
98
140
 
99
- const actual = node?.root?.value?.[targetField];
100
- const isEmpty = value === '' || value === null || value === undefined;
141
+ const actual = node?.root?.value?.[targetField];
142
+ const isEmpty = value === '' || value === null || value === undefined;
101
143
 
102
- if (actual === expectedValue && isEmpty) {
103
- return false;
104
- }
144
+ if (actual === expectedValue && isEmpty) {
145
+ return false;
146
+ }
105
147
 
106
- return true;
148
+ return true;
107
149
  }
108
150
 
109
151
  class MarkdownRenderer extends marked.Renderer {
110
- link(params) {
111
- const link = super.link(params);
112
- return link.replace('<a', "<a target='_blank'");
113
- }
152
+ link(params) {
153
+ const link = super.link(params);
154
+ return link.replace('<a', "<a target='_blank'");
155
+ }
114
156
 
115
- html(params) {
116
- const result = super.html(params);
117
- if (result.startsWith('<a ') && !result.includes('target=')) {
118
- return result.replace('<a ', `<a target="_blank" `);
119
- }
120
- return result;
157
+ html(params) {
158
+ const result = super.html(params);
159
+ if (result.startsWith('<a ') && !result.includes('target=')) {
160
+ return result.replace('<a ', `<a target="_blank" `);
121
161
  }
162
+ return result;
163
+ }
122
164
  }
123
165
 
124
166
  class MarkedHooks extends marked.Hooks {
125
- postprocess(html) {
126
- return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
127
- }
167
+ postprocess(html) {
168
+ return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
169
+ }
128
170
  }
129
171
 
130
172
  function processMarkdown(content) {
131
- if (!content) return '';
173
+ if (!content) return '';
132
174
 
133
- const html = marked.parse(content.toString(), {
134
- renderer: new MarkdownRenderer(),
135
- hooks: new MarkedHooks(),
136
- });
175
+ const html = marked.parse(content.toString(), {
176
+ renderer: new MarkdownRenderer(),
177
+ hooks: new MarkedHooks(),
178
+ });
137
179
 
138
- return html;
180
+ return html;
139
181
  }
140
182
 
141
183
  export default {
142
- name: 'UIDynamicForm',
143
- components: {
144
- FormKit,
145
- UIDynamicFormFooterAction,
146
- UIDynamicFormTitleText,
147
- },
148
- inject: ['$socket'],
149
- props: {
150
- /* do not remove entries from this - Dashboard's Layout Manager's will pass this data to your component */
151
- id: { type: String, required: true },
152
- props: { type: Object, default: () => ({}) },
153
- state: {
154
- type: Object,
155
- default: () => ({ enabled: false, visible: false }),
156
- },
184
+ name: 'UIDynamicForm',
185
+ components: {
186
+ FormKit,
187
+ UIDynamicFormFooterAction,
188
+ UIDynamicFormTitleText,
189
+ },
190
+ inject: ['$socket'],
191
+ props: {
192
+ id: { type: String, required: true },
193
+ props: { type: Object, default: () => ({}) },
194
+ state: {
195
+ type: Object,
196
+ default: () => ({ enabled: false, visible: false }),
157
197
  },
158
- setup(props) {
159
- console.info('UIDynamicForm setup with:', props);
160
- console.debug('Vue function loaded correctly', markRaw);
198
+ },
199
+ setup(props) {
200
+ console.info('UIDynamicForm setup with:', props);
201
+ console.debug('Vue function loaded correctly', markRaw);
161
202
 
162
- const instance = getCurrentInstance();
163
- const app = instance.appContext.app;
203
+ const instance = getCurrentInstance();
204
+ const app = instance.appContext.app;
164
205
 
165
- const formkitConfig = defaultConfig({
166
- theme: 'genesis',
167
- locales: { de },
168
- locale: 'de',
169
- // eslint-disable-next-line object-shorthand
170
- rules: { requiredIf: requiredIf },
171
- });
172
- app.use(plugin, formkitConfig);
206
+ const formkitConfig = defaultConfig({
207
+ theme: 'genesis',
208
+ locales: { de },
209
+ locale: 'de',
210
+ rules: { requiredIf: requiredIf },
211
+ });
212
+ app.use(plugin, formkitConfig);
213
+ },
214
+ data() {
215
+ return {
216
+ actions: [],
217
+ formData: {},
218
+ userTask: null,
219
+ theme: '',
220
+ errorMsg: '',
221
+ formIsFinished: false,
222
+ msg: null,
223
+ collapsed: false,
224
+ firstFormFieldRef: null,
225
+ };
226
+ },
227
+ computed: {
228
+ dynamicClass() {
229
+ return `ui-dynamic-form-${this.theme} ui-dynamic-form-common`;
173
230
  },
174
- data() {
175
- return {
176
- actions: [],
177
- formData: {},
178
- userTask: null,
179
- theme: '',
180
- errorMsg: '',
181
- formIsFinished: false,
182
- msg: null,
183
- collapsed: false,
184
- firstFormFieldRef: null,
185
- };
231
+ dynamicFooterClass() {
232
+ return `ui-dynamic-form-footer-${this.theme} ui-dynamic-form-footer-common`;
186
233
  },
187
- computed: {
188
- dynamicClass() {
189
- return `ui-dynamic-form-${this.theme} ui-dynamic-form-common`;
190
- },
191
- dynamicFooterClass() {
192
- return `ui-dynamic-form-footer-${this.theme} ui-dynamic-form-footer-common`;
193
- },
194
- hasUserTask() {
195
- return !!this.userTask;
196
- },
197
- totalOutputs() {
198
- return (
199
- this.props.options.length +
200
- (this.props.handle_confirmation_dialogs ? 2 : 0) +
201
- (this.props.trigger_on_change ? 1 : 0)
202
- );
203
- },
204
- isConfirmDialog() {
205
- return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
206
- },
207
- effectiveTitle() {
208
- if (this.props.title_text_type === 'str') {
209
- return this.props.title_text;
210
- } else if (this.props.title_text_type === 'msg') {
211
- return this.msg.dynamicTitle;
212
- } else {
213
- return '';
214
- }
215
- },
234
+ hasUserTask() {
235
+ return !!this.userTask;
216
236
  },
217
- watch: {
218
- formData: {
219
- handler(newData, oldData) {
220
- if (this.props.trigger_on_change) {
221
- const res = { payload: { formData: newData, userTask: this.userTask } };
222
- this.send(res, this.totalOutputs - 1);
223
- }
224
- },
225
- collapsed(newVal) {
226
- if (!newVal && this.hasUserTask) {
227
- nextTick(() => {
228
- this.focusFirstFormField();
229
- });
230
- }
231
- },
232
- userTask(newVal) {
233
- if (newVal && !this.collapsed) {
234
- nextTick(() => {
235
- this.focusFirstFormField();
236
- });
237
- }
238
- },
239
- deep: true,
240
- },
237
+ totalOutputs() {
238
+ const outputsConfirmTerminate = 2;
239
+ return (
240
+ this.props.options.length +
241
+ (this.props.handle_confirmation_dialogs ? 2 : 0) +
242
+ (this.props.trigger_on_change ? 1 : 0) +
243
+ outputsConfirmTerminate
244
+ );
245
+ },
246
+ isConfirmDialog() {
247
+ return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
248
+ },
249
+ effectiveTitle() {
250
+ if (this.props.title_text_type === 'str') {
251
+ return this.props.title_text;
252
+ } else if (this.props.title_text_type === 'msg') {
253
+ return this.msg.dynamicTitle;
254
+ } else {
255
+ return '';
256
+ }
257
+ },
258
+ },
259
+ watch: {
260
+ formData: {
261
+ handler(newData, oldData) {
262
+ if (this.props.trigger_on_change) {
263
+ const res = { payload: { formData: newData, userTask: this.userTask } };
264
+ this.send(res, this.totalOutputs - 1);
265
+ }
266
+ },
267
+ collapsed(newVal) {
268
+ if (!newVal && this.hasUserTask) {
269
+ nextTick(() => {
270
+ this.focusFirstFormField();
271
+ });
272
+ }
273
+ },
274
+ userTask(newVal) {
275
+ if (newVal && !this.collapsed) {
276
+ nextTick(() => {
277
+ this.focusFirstFormField();
278
+ });
279
+ }
280
+ },
281
+ deep: true,
241
282
  },
242
- created() {
243
- const currentPath = window.location.pathname;
244
- const lastPart = currentPath.substring(currentPath.lastIndexOf('/'));
283
+ },
284
+ created() {
285
+ const currentPath = window.location.pathname;
286
+ const lastPart = currentPath.substring(currentPath.lastIndexOf('/'));
245
287
 
246
- const store = this.$store.state;
288
+ const store = this.$store.state;
247
289
 
248
- for (const key in store.ui.pages) {
249
- if (store.ui.pages[key].path === lastPart) {
250
- const theme = store.ui.pages[key].theme;
251
- if (store.ui.themes[theme].name === 'ProcessCube Lightmode') {
252
- this.theme = 'light';
253
- } else if (store.ui.themes[theme].name === 'ProcessCube Darkmode') {
254
- this.theme = 'dark';
255
- } else {
256
- this.theme = 'default';
257
- }
258
- break;
259
- }
290
+ for (const key in store.ui.pages) {
291
+ if (store.ui.pages[key].path === lastPart) {
292
+ const theme = store.ui.pages[key].theme;
293
+ if (store.ui.themes[theme].name === 'ProcessCube Lightmode') {
294
+ this.theme = 'light';
295
+ } else if (store.ui.themes[theme].name === 'ProcessCube Darkmode') {
296
+ this.theme = 'dark';
297
+ } else {
298
+ this.theme = 'default';
260
299
  }
261
- },
262
- mounted() {
263
- const elements = document.querySelectorAll('.formkit-input');
300
+ break;
301
+ }
302
+ }
303
+ },
304
+ mounted() {
305
+ const elements = document.querySelectorAll('.formkit-input');
264
306
 
265
- elements.forEach((element) => {
266
- element.classList.add('test');
267
- });
307
+ elements.forEach((element) => {
308
+ element.classList.add('test');
309
+ });
268
310
 
269
- this.$socket.on('widget-load:' + this.id, (msg) => {
270
- this.init(msg);
271
- });
272
- this.$socket.on('msg-input:' + this.id, (msg) => {
273
- // store the latest message in our client-side vuex store when we receive a new message
274
- this.init(msg);
275
- });
276
- // tell Node-RED that we're loading a new instance of this widget
277
- this.$socket.emit('widget-load', this.id);
311
+ this.$socket.on('widget-load:' + this.id, (msg) => {
312
+ this.init(msg);
313
+ });
314
+ this.$socket.on('msg-input:' + this.id, (msg) => {
315
+ this.init(msg);
316
+ });
317
+ this.$socket.emit('widget-load', this.id);
318
+ },
319
+ unmounted() {
320
+ this.$socket?.off('widget-load' + this.id);
321
+ this.$socket?.off('msg-input:' + this.id);
322
+ },
323
+ methods: {
324
+ createComponent(field) {
325
+ const customForm = field.customForm ? JSON.parse(JSON.stringify(field.customForm)) : {};
326
+ const hint = customForm.hint;
327
+ const placeholder = customForm.placeholder;
328
+ const validation = customForm.validation;
329
+ const name = field.id;
330
+ const customProperties = customForm.customProperties ?? [];
331
+ const isReadOnly =
332
+ this.props.readonly ||
333
+ this.formIsFinished ||
334
+ customProperties.find((entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true')
335
+ ? 'true'
336
+ : undefined;
337
+ switch (field.type) {
338
+ case 'long':
339
+ return {
340
+ type: 'FormKit',
341
+ props: {
342
+ type: 'number',
343
+ id: field.id,
344
+ name,
345
+ label: field.label,
346
+ required: field.required,
347
+ value: this.formData[field.id],
348
+ number: 'integer',
349
+ min: 0,
350
+ validation: validation ? `${validation}|number` : 'number',
351
+ help: hint,
352
+ wrapperClass: '$remove:formkit-wrapper',
353
+ labelClass: 'ui-dynamic-form-input-label',
354
+ inputClass: `input-${this.theme}`,
355
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
356
+ readonly: isReadOnly,
357
+ validationVisibility: 'live',
358
+ },
359
+ };
360
+ case 'number':
361
+ const step = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).step : undefined;
362
+ return {
363
+ type: 'FormKit',
364
+ props: {
365
+ type: 'number',
366
+ id: field.id,
367
+ name,
368
+ label: field.label,
369
+ required: field.required,
370
+ value: this.formData[field.id],
371
+ step,
372
+ number: 'float',
373
+ validation: validation ? `${validation}|number` : 'number',
374
+ help: hint,
375
+ wrapperClass: '$remove:formkit-wrapper',
376
+ labelClass: 'ui-dynamic-form-input-label',
377
+ inputClass: `input-${this.theme}`,
378
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
379
+ readonly: isReadOnly,
380
+ validationVisibility: 'live',
381
+ },
382
+ };
383
+ case 'date':
384
+ return {
385
+ type: 'FormKit',
386
+ props: {
387
+ type: 'date',
388
+ id: field.id,
389
+ name,
390
+ label: field.label,
391
+ required: field.required,
392
+ value: this.formData[field.id],
393
+ help: hint,
394
+ wrapperClass: '$remove:formkit-wrapper',
395
+ labelClass: 'ui-dynamic-form-input-label',
396
+ inputClass: `input-${this.theme}`,
397
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
398
+ readonly: isReadOnly,
399
+ validation,
400
+ validationVisibility: 'live',
401
+ },
402
+ };
403
+ case 'enum':
404
+ const enums = field.enumValues.map((obj) => {
405
+ return { value: obj.id, label: obj.name };
406
+ });
407
+ return {
408
+ type: 'FormKit',
409
+ props: {
410
+ type: 'select',
411
+ id: field.id,
412
+ name,
413
+ label: field.label,
414
+ required: field.required,
415
+ value: this.formData[field.id],
416
+ options: enums,
417
+ help: hint,
418
+ wrapperClass: '$remove:formkit-wrapper',
419
+ labelClass: 'ui-dynamic-form-input-label',
420
+ inputClass: `input-${this.theme}`,
421
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
422
+ readonly: isReadOnly,
423
+ disabled: isReadOnly,
424
+ validation,
425
+ validationVisibility: 'live',
426
+ },
427
+ };
428
+ case 'select':
429
+ const selections = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
430
+ return { value: obj.key, label: obj.value };
431
+ });
432
+ return {
433
+ type: 'FormKit',
434
+ props: {
435
+ type: 'select',
436
+ id: field.id,
437
+ name,
438
+ label: field.label,
439
+ required: field.required,
440
+ value: this.formData[field.id],
441
+ options: selections,
442
+ placeholder,
443
+ help: hint,
444
+ wrapperClass: '$remove:formkit-wrapper',
445
+ labelClass: 'ui-dynamic-form-input-label',
446
+ inputClass: `input-${this.theme}`,
447
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
448
+ readonly: isReadOnly,
449
+ disabled: isReadOnly,
450
+ validation,
451
+ validationVisibility: 'live',
452
+ },
453
+ };
454
+ case 'string':
455
+ return {
456
+ type: 'FormKit',
457
+ props: {
458
+ type: 'text',
459
+ id: field.id,
460
+ name,
461
+ label: field.label,
462
+ required: field.required,
463
+ value: this.formData[field.id],
464
+ help: hint,
465
+ placeholder,
466
+ wrapperClass: '$remove:formkit-wrapper',
467
+ labelClass: 'ui-dynamic-form-input-label',
468
+ inputClass: `input-${this.theme}`,
469
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
470
+ readonly: isReadOnly,
471
+ validation,
472
+ validationVisibility: 'live',
473
+ },
474
+ };
475
+ case 'confirm':
476
+ return {
477
+ type: 'h3',
478
+ innerText: field.label,
479
+ };
480
+ case 'boolean':
481
+ return {
482
+ type: 'FormKit',
483
+ props: {
484
+ type: 'checkbox',
485
+ id: field.id,
486
+ name,
487
+ label: field.label,
488
+ required: field.required,
489
+ value: this.formData[field.id],
490
+ help: hint,
491
+ labelClass: 'ui-dynamic-form-input-label',
492
+ inputClass: `input-${this.theme}`,
493
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
494
+ readonly: isReadOnly,
495
+ disabled: isReadOnly,
496
+ validation,
497
+ validationVisibility: 'live',
498
+ },
499
+ };
500
+ case 'file':
501
+ const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
502
+ return {
503
+ type: 'FormKit',
504
+ props: {
505
+ type: 'file',
506
+ id: field.id,
507
+ name,
508
+ label: field.label,
509
+ required: field.required,
510
+ value: this.formData[field.id],
511
+ help: hint,
512
+ innerClass: 'reset-background',
513
+ wrapperClass: '$remove:formkit-wrapper',
514
+ labelClass: 'ui-dynamic-form-input-label',
515
+ inputClass: `input-${this.theme}`,
516
+ readonly: isReadOnly,
517
+ disabled: isReadOnly,
518
+ multiple,
519
+ validation,
520
+ validationVisibility: 'live',
521
+ },
522
+ };
523
+ case 'checkbox':
524
+ const options = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
525
+ return { value: obj.key, label: obj.value };
526
+ });
527
+ return {
528
+ type: 'FormKit',
529
+ props: {
530
+ type: 'checkbox',
531
+ id: field.id,
532
+ name,
533
+ label: field.label,
534
+ required: field.required,
535
+ value: this.formData[field.id],
536
+ options,
537
+ help: hint,
538
+ fieldsetClass: 'custom-fieldset',
539
+ labelClass: 'ui-dynamic-form-input-label',
540
+ inputClass: `input-${this.theme}`,
541
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
542
+ readonly: isReadOnly,
543
+ disabled: isReadOnly,
544
+ validation,
545
+ validationVisibility: 'live',
546
+ },
547
+ };
548
+ case 'color':
549
+ return {
550
+ type: 'FormKit',
551
+ props: {
552
+ type: 'color',
553
+ id: field.id,
554
+ name,
555
+ label: field.label,
556
+ required: field.required,
557
+ value: this.formData[field.id],
558
+ help: hint,
559
+ readonly: isReadOnly,
560
+ disabled: isReadOnly,
561
+ validation,
562
+ validationVisibility: 'live',
563
+ },
564
+ };
565
+ case 'datetime-local':
566
+ return {
567
+ type: 'FormKit',
568
+ props: {
569
+ type: 'datetime-local',
570
+ id: field.id,
571
+ name,
572
+ label: field.label,
573
+ required: field.required,
574
+ value: this.formData[field.id],
575
+ help: hint,
576
+ wrapperClass: '$remove:formkit-wrapper',
577
+ labelClass: 'ui-dynamic-form-input-label',
578
+ inputClass: `input-${this.theme}`,
579
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
580
+ readonly: isReadOnly,
581
+ validation,
582
+ validationVisibility: 'live',
583
+ },
584
+ };
585
+ case 'email':
586
+ return {
587
+ type: 'FormKit',
588
+ props: {
589
+ type: 'email',
590
+ id: field.id,
591
+ name,
592
+ label: field.label,
593
+ required: field.required,
594
+ value: this.formData[field.id],
595
+ help: hint,
596
+ placeholder,
597
+ wrapperClass: '$remove:formkit-wrapper',
598
+ labelClass: 'ui-dynamic-form-input-label',
599
+ inputClass: `input-${this.theme}`,
600
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
601
+ readonly: isReadOnly,
602
+ validation,
603
+ validationVisibility: 'live',
604
+ },
605
+ };
606
+ case 'header':
607
+ let typeToUse = 'h1';
608
+ if (field.customForm && JSON.parse(JSON.stringify(field.customForm)).style === 'heading_2') {
609
+ typeToUse = 'h2';
610
+ }
611
+ if (field.customForm && JSON.parse(JSON.stringify(field.customForm)).style === 'heading_3') {
612
+ typeToUse = 'h3';
613
+ }
614
+ return {
615
+ type: typeToUse,
616
+ innerText: this.formData[field.id],
617
+ };
618
+ case 'hidden':
619
+ return {
620
+ type: 'input',
621
+ props: {
622
+ type: 'hidden',
623
+ value: this.formData[field.id],
624
+ },
625
+ };
626
+ case 'month':
627
+ return {
628
+ type: 'FormKit',
629
+ props: {
630
+ type: 'month',
631
+ id: field.id,
632
+ name,
633
+ label: field.label,
634
+ required: field.required,
635
+ value: this.formData[field.id],
636
+ help: hint,
637
+ wrapperClass: '$remove:formkit-wrapper',
638
+ labelClass: 'ui-dynamic-form-input-label',
639
+ inputClass: `input-${this.theme}`,
640
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
641
+ readonly: isReadOnly,
642
+ validation,
643
+ validationVisibility: 'live',
644
+ },
645
+ };
646
+ case 'paragraph':
647
+ const paragraphContent = this.formData[field.id] || field.defaultValue || field.label || '';
648
+ const processedHtml = processMarkdown(paragraphContent);
649
+ return {
650
+ type: 'div',
651
+ innerHTML: processedHtml,
652
+ class: 'ui-dynamic-form-paragraph',
653
+ };
654
+ case 'password':
655
+ return {
656
+ type: 'FormKit',
657
+ props: {
658
+ type: 'password',
659
+ id: field.id,
660
+ name,
661
+ label: field.label,
662
+ required: field.required,
663
+ value: this.formData[field.id],
664
+ help: hint,
665
+ placeholder,
666
+ wrapperClass: '$remove:formkit-wrapper',
667
+ labelClass: 'ui-dynamic-form-input-label',
668
+ inputClass: `input-${this.theme}`,
669
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
670
+ readonly: isReadOnly,
671
+ validation,
672
+ validationVisibility: 'live',
673
+ },
674
+ };
675
+ case 'radio':
676
+ const radioOptions = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
677
+ return { value: obj.key, label: obj.value };
678
+ });
679
+ return {
680
+ type: 'FormKit',
681
+ props: {
682
+ type: 'radio',
683
+ id: field.id,
684
+ name,
685
+ label: field.label,
686
+ required: field.required,
687
+ value: this.formData[field.id],
688
+ options: radioOptions,
689
+ help: hint,
690
+ fieldsetClass: 'custom-fieldset',
691
+ labelClass: 'ui-dynamic-form-input-label',
692
+ inputClass: `input-${this.theme}`,
693
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
694
+ readonly: isReadOnly,
695
+ disabled: isReadOnly,
696
+ validation,
697
+ validationVisibility: 'live',
698
+ },
699
+ };
700
+ case 'range':
701
+ const customForm = JSON.parse(JSON.stringify(field.customForm));
702
+ return {
703
+ type: 'v-slider',
704
+ props: {
705
+ id: field.id,
706
+ name,
707
+ required: field.required,
708
+ min: customForm.min,
709
+ max: customForm.max,
710
+ step: customForm.step,
711
+ thumbLabel: true,
712
+ labelClass: 'ui-dynamic-form-input-label',
713
+ readonly: isReadOnly,
714
+ disabled: isReadOnly,
715
+ validation,
716
+ validationVisibility: 'live',
717
+ },
718
+ };
719
+ case 'tel':
720
+ return {
721
+ type: 'FormKit',
722
+ props: {
723
+ type: 'tel',
724
+ id: field.id,
725
+ name,
726
+ label: field.label,
727
+ required: field.required,
728
+ value: this.formData[field.id],
729
+ help: hint,
730
+ placeholder,
731
+ wrapperClass: '$remove:formkit-wrapper',
732
+ labelClass: 'ui-dynamic-form-input-label',
733
+ inputClass: `input-${this.theme}`,
734
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
735
+ readonly: isReadOnly,
736
+ validation,
737
+ validationVisibility: 'live',
738
+ },
739
+ };
740
+ case 'textarea':
741
+ const rows = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).rows : undefined;
742
+ return {
743
+ type: 'FormKit',
744
+ props: {
745
+ type: 'textarea',
746
+ id: field.id,
747
+ name,
748
+ label: field.label,
749
+ required: field.required,
750
+ value: this.formData[field.id],
751
+ rows,
752
+ help: hint,
753
+ placeholder,
754
+ wrapperClass: '$remove:formkit-wrapper',
755
+ labelClass: 'ui-dynamic-form-input-label',
756
+ inputClass: `input-${this.theme}`,
757
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
758
+ readonly: isReadOnly,
759
+ validation,
760
+ validationVisibility: 'live',
761
+ },
762
+ };
763
+ case 'time':
764
+ return {
765
+ type: 'FormKit',
766
+ props: {
767
+ type: 'time',
768
+ id: field.id,
769
+ name,
770
+ label: field.label,
771
+ required: field.required,
772
+ value: this.formData[field.id],
773
+ help: hint,
774
+ placeholder,
775
+ wrapperClass: '$remove:formkit-wrapper',
776
+ labelClass: 'ui-dynamic-form-input-label',
777
+ inputClass: `input-${this.theme}`,
778
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
779
+ readonly: isReadOnly,
780
+ validation,
781
+ validationVisibility: 'live',
782
+ },
783
+ };
784
+ case 'url':
785
+ return {
786
+ type: 'FormKit',
787
+ props: {
788
+ type: 'url',
789
+ id: field.id,
790
+ name,
791
+ label: field.label,
792
+ required: field.required,
793
+ value: this.formData[field.id],
794
+ help: hint,
795
+ placeholder,
796
+ wrapperClass: '$remove:formkit-wrapper',
797
+ labelClass: 'ui-dynamic-form-input-label',
798
+ inputClass: `input-${this.theme}`,
799
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
800
+ readonly: isReadOnly,
801
+ validation,
802
+ validationVisibility: 'live',
803
+ },
804
+ };
805
+ case 'week':
806
+ return {
807
+ type: 'FormKit',
808
+ props: {
809
+ type: 'week',
810
+ id: field.id,
811
+ name,
812
+ label: field.label,
813
+ required: field.required,
814
+ value: this.formData[field.id],
815
+ help: hint,
816
+ placeholder,
817
+ wrapperClass: '$remove:formkit-wrapper',
818
+ labelClass: 'ui-dynamic-form-input-label',
819
+ inputClass: `input-${this.theme}`,
820
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
821
+ readonly: isReadOnly,
822
+ validation,
823
+ validationVisibility: 'live',
824
+ },
825
+ };
826
+ default:
827
+ return {
828
+ type: 'FormKit',
829
+ props: {
830
+ type: field.type,
831
+ id: field.id,
832
+ name,
833
+ label: field.label,
834
+ required: field.required,
835
+ value: this.formData[field.id],
836
+ help: hint,
837
+ labelClass: 'ui-dynamic-form-input-label',
838
+ inputClass: `input-${this.theme}`,
839
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
840
+ readonly: isReadOnly,
841
+ validation,
842
+ validationVisibility: 'live',
843
+ },
844
+ };
845
+ }
278
846
  },
279
- unmounted() {
280
- /* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
281
- this.$socket?.off('widget-load' + this.id);
282
- this.$socket?.off('msg-input:' + this.id);
847
+ toggleCollapse() {
848
+ this.collapsed = !this.collapsed;
283
849
  },
284
- methods: {
285
- createComponent(field) {
286
- console.debug('Creating component for field:', field);
287
- const customForm = field.customForm ? JSON.parse(field.customForm) : {};
288
- const hint = customForm.hint;
289
- const placeholder = customForm.placeholder;
290
- const validation = customForm.validation;
291
- const name = field.id;
292
- const customProperties = customForm.customProperties ?? [];
293
- const isReadOnly =
294
- this.props.readonly ||
295
- this.formIsFinished ||
296
- customProperties.find((entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true')
297
- ? 'true'
298
- : undefined;
299
- switch (field.type) {
300
- case 'long':
301
- return {
302
- type: 'FormKit',
303
- props: {
304
- type: 'number',
305
- id: field.id,
306
- name,
307
- label: field.label,
308
- required: field.required,
309
- value: this.formData[field.id],
310
- number: 'integer',
311
- min: 0,
312
- validation: validation ? `${validation}|number` : 'number',
313
- help: hint,
314
- wrapperClass: '$remove:formkit-wrapper',
315
- labelClass: 'ui-dynamic-form-input-label',
316
- inputClass: `input-${this.theme}`,
317
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
318
- readonly: isReadOnly,
319
- validationVisibility: 'live',
320
- },
321
- };
322
- case 'number':
323
- const step = field.customForm ? JSON.parse(field.customForm).step : undefined;
324
- return {
325
- type: 'FormKit',
326
- props: {
327
- type: 'number',
328
- id: field.id,
329
- name,
330
- label: field.label,
331
- required: field.required,
332
- value: this.formData[field.id],
333
- step,
334
- number: 'float',
335
- validation: validation ? `${validation}|number` : 'number',
336
- help: hint,
337
- wrapperClass: '$remove:formkit-wrapper',
338
- labelClass: 'ui-dynamic-form-input-label',
339
- inputClass: `input-${this.theme}`,
340
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
341
- readonly: isReadOnly,
342
- validationVisibility: 'live',
343
- },
344
- };
345
- case 'date':
346
- return {
347
- type: 'FormKit',
348
- props: {
349
- type: 'date',
350
- id: field.id,
351
- name,
352
- label: field.label,
353
- required: field.required,
354
- value: this.formData[field.id],
355
- help: hint,
356
- wrapperClass: '$remove:formkit-wrapper',
357
- labelClass: 'ui-dynamic-form-input-label',
358
- inputClass: `input-${this.theme}`,
359
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
360
- readonly: isReadOnly,
361
- validation,
362
- validationVisibility: 'live',
363
- },
364
- };
365
- case 'enum':
366
- const enums = field.enumValues.map((obj) => {
367
- return { value: obj.id, label: obj.name };
368
- });
369
- return {
370
- type: 'FormKit',
371
- props: {
372
- type: 'select', // JSON.parse(field.customForm).displayAs
373
- id: field.id,
374
- name,
375
- label: field.label,
376
- required: field.required,
377
- value: this.formData[field.id],
378
- options: enums,
379
- help: hint,
380
- wrapperClass: '$remove:formkit-wrapper',
381
- labelClass: 'ui-dynamic-form-input-label',
382
- inputClass: `input-${this.theme}`,
383
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
384
- readonly: isReadOnly,
385
- disabled: isReadOnly,
386
- validation,
387
- validationVisibility: 'live',
388
- },
389
- };
390
- case 'select':
391
- const selections = JSON.parse(field.customForm).entries.map((obj) => {
392
- return { value: obj.key, label: obj.value };
393
- });
394
- return {
395
- type: 'FormKit',
396
- props: {
397
- type: 'select', // JSON.parse(field.customForm).displayAs
398
- id: field.id,
399
- name,
400
- label: field.label,
401
- required: field.required,
402
- value: this.formData[field.id],
403
- options: selections,
404
- placeholder,
405
- help: hint,
406
- wrapperClass: '$remove:formkit-wrapper',
407
- labelClass: 'ui-dynamic-form-input-label',
408
- inputClass: `input-${this.theme}`,
409
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
410
- readonly: isReadOnly,
411
- disabled: isReadOnly,
412
- validation,
413
- validationVisibility: 'live',
414
- },
415
- };
416
- case 'string':
417
- return {
418
- type: 'FormKit',
419
- props: {
420
- type: 'text',
421
- id: field.id,
422
- name,
423
- label: field.label,
424
- required: field.required,
425
- value: this.formData[field.id],
426
- help: hint,
427
- placeholder,
428
- wrapperClass: '$remove:formkit-wrapper',
429
- labelClass: 'ui-dynamic-form-input-label',
430
- inputClass: `input-${this.theme}`,
431
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
432
- readonly: isReadOnly,
433
- validation,
434
- validationVisibility: 'live',
435
- },
436
- };
437
- case 'confirm':
438
- return {
439
- type: 'h3',
440
- innerText: field.label,
441
- };
442
- case 'boolean':
443
- return {
444
- type: 'FormKit',
445
- props: {
446
- type: 'checkbox',
447
- id: field.id,
448
- name,
449
- label: field.label,
450
- required: field.required,
451
- value: this.formData[field.id],
452
- help: hint,
453
- labelClass: 'ui-dynamic-form-input-label',
454
- inputClass: `input-${this.theme}`,
455
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
456
- readonly: isReadOnly,
457
- disabled: isReadOnly,
458
- validation,
459
- validationVisibility: 'live',
460
- },
461
- };
462
- case 'file':
463
- const multiple = field.customForm ? JSON.parse(field.customForm).multiple === 'true' : false;
464
- return {
465
- type: 'FormKit',
466
- props: {
467
- type: 'file',
468
- id: field.id,
469
- name,
470
- label: field.label,
471
- required: field.required,
472
- value: this.formData[field.id],
473
- help: hint,
474
- innerClass: 'reset-background',
475
- wrapperClass: '$remove:formkit-wrapper',
476
- labelClass: 'ui-dynamic-form-input-label',
477
- inputClass: `input-${this.theme}`,
478
- // innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
479
- readonly: isReadOnly,
480
- disabled: isReadOnly,
481
- multiple,
482
- validation,
483
- validationVisibility: 'live',
484
- },
485
- };
486
- case 'checkbox':
487
- const options = JSON.parse(field.customForm).entries.map((obj) => {
488
- return { value: obj.key, label: obj.value };
489
- });
490
- return {
491
- type: 'FormKit',
492
- props: {
493
- type: 'checkbox',
494
- id: field.id,
495
- name,
496
- label: field.label,
497
- required: field.required,
498
- value: this.formData[field.id],
499
- options,
500
- help: hint,
501
- fieldsetClass: 'custom-fieldset',
502
- labelClass: 'ui-dynamic-form-input-label',
503
- inputClass: `input-${this.theme}`,
504
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
505
- readonly: isReadOnly,
506
- disabled: isReadOnly,
507
- validation,
508
- validationVisibility: 'live',
509
- },
510
- };
511
- case 'color':
512
- return {
513
- type: 'FormKit',
514
- props: {
515
- type: 'color',
516
- id: field.id,
517
- name,
518
- label: field.label,
519
- required: field.required,
520
- value: this.formData[field.id],
521
- help: hint,
522
- readonly: isReadOnly,
523
- disabled: isReadOnly,
524
- validation,
525
- validationVisibility: 'live',
526
- },
527
- };
528
- case 'datetime-local':
529
- return {
530
- type: 'FormKit',
531
- props: {
532
- type: 'datetime-local',
533
- id: field.id,
534
- name,
535
- label: field.label,
536
- required: field.required,
537
- value: this.formData[field.id],
538
- help: hint,
539
- wrapperClass: '$remove:formkit-wrapper',
540
- labelClass: 'ui-dynamic-form-input-label',
541
- inputClass: `input-${this.theme}`,
542
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
543
- readonly: isReadOnly,
544
- validation,
545
- validationVisibility: 'live',
546
- },
547
- };
548
- case 'email':
549
- return {
550
- type: 'FormKit',
551
- props: {
552
- type: 'email',
553
- id: field.id,
554
- name,
555
- label: field.label,
556
- required: field.required,
557
- value: this.formData[field.id],
558
- help: hint,
559
- placeholder,
560
- wrapperClass: '$remove:formkit-wrapper',
561
- labelClass: 'ui-dynamic-form-input-label',
562
- inputClass: `input-${this.theme}`,
563
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
564
- readonly: isReadOnly,
565
- validation,
566
- validationVisibility: 'live',
567
- },
568
- };
569
- case 'header':
570
- let typeToUse = 'h1';
571
- if (field.customForm && JSON.parse(field.customForm).style === 'heading_2') {
572
- typeToUse = 'h2';
573
- }
574
- if (field.customForm && JSON.parse(field.customForm).style === 'heading_3') {
575
- typeToUse = 'h3';
576
- }
577
- return {
578
- type: typeToUse,
579
- innerText: this.formData[field.id],
580
- };
581
- case 'hidden':
582
- return {
583
- type: 'input',
584
- props: {
585
- type: 'hidden',
586
- value: this.formData[field.id],
587
- },
588
- };
589
- case 'month':
590
- return {
591
- type: 'FormKit',
592
- props: {
593
- type: 'month',
594
- id: field.id,
595
- name,
596
- label: field.label,
597
- required: field.required,
598
- value: this.formData[field.id],
599
- help: hint,
600
- wrapperClass: '$remove:formkit-wrapper',
601
- labelClass: 'ui-dynamic-form-input-label',
602
- inputClass: `input-${this.theme}`,
603
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
604
- readonly: isReadOnly,
605
- validation,
606
- validationVisibility: 'live',
607
- },
608
- };
609
- case 'paragraph':
610
- const paragraphContent = this.formData[field.id] || field.defaultValue || field.label || '';
611
- const processedHtml = processMarkdown(paragraphContent);
612
- return {
613
- type: 'div',
614
- innerHTML: processedHtml,
615
- class: 'ui-dynamic-form-paragraph',
616
- };
617
- case 'password':
618
- return {
619
- type: 'FormKit',
620
- props: {
621
- type: 'password',
622
- id: field.id,
623
- name,
624
- label: field.label,
625
- required: field.required,
626
- value: this.formData[field.id],
627
- help: hint,
628
- placeholder,
629
- wrapperClass: '$remove:formkit-wrapper',
630
- labelClass: 'ui-dynamic-form-input-label',
631
- inputClass: `input-${this.theme}`,
632
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
633
- readonly: isReadOnly,
634
- validation,
635
- validationVisibility: 'live',
636
- },
637
- };
638
- case 'radio':
639
- const radioOptions = JSON.parse(field.customForm).entries.map((obj) => {
640
- return { value: obj.key, label: obj.value };
641
- });
642
- return {
643
- type: 'FormKit',
644
- props: {
645
- type: 'radio',
646
- id: field.id,
647
- name,
648
- label: field.label,
649
- required: field.required,
650
- value: this.formData[field.id],
651
- options: radioOptions,
652
- help: hint,
653
- fieldsetClass: 'custom-fieldset',
654
- labelClass: 'ui-dynamic-form-input-label',
655
- inputClass: `input-${this.theme}`,
656
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
657
- readonly: isReadOnly,
658
- disabled: isReadOnly,
659
- validation,
660
- validationVisibility: 'live',
661
- },
662
- };
663
- case 'range':
664
- const customForm = JSON.parse(field.customForm);
665
- return {
666
- type: 'v-slider',
667
- props: {
668
- id: field.id,
669
- name,
670
- // label: field.label,
671
- required: field.required,
672
- // value: this.formData[field.id],
673
- // help: hint,
674
- min: customForm.min,
675
- max: customForm.max,
676
- step: customForm.step,
677
- thumbLabel: true,
678
- // wrapperClass: '$remove:formkit-wrapper',
679
- labelClass: 'ui-dynamic-form-input-label',
680
- // inputClass: `input-${this.theme}`,
681
- // innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
682
- readonly: isReadOnly,
683
- disabled: isReadOnly,
684
- validation,
685
- validationVisibility: 'live',
686
- },
687
- };
688
- case 'tel':
689
- return {
690
- type: 'FormKit',
691
- props: {
692
- type: 'tel' /* with pro component mask more good */,
693
- id: field.id,
694
- name,
695
- label: field.label,
696
- required: field.required,
697
- value: this.formData[field.id],
698
- help: hint,
699
- placeholder,
700
- wrapperClass: '$remove:formkit-wrapper',
701
- labelClass: 'ui-dynamic-form-input-label',
702
- inputClass: `input-${this.theme}`,
703
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
704
- readonly: isReadOnly,
705
- validation,
706
- validationVisibility: 'live',
707
- },
708
- };
709
- case 'textarea':
710
- const rows = field.customForm ? JSON.parse(field.customForm).rows : undefined;
711
- return {
712
- type: 'FormKit',
713
- props: {
714
- type: 'textarea' /* with pro component mask more good */,
715
- id: field.id,
716
- name,
717
- label: field.label,
718
- required: field.required,
719
- value: this.formData[field.id],
720
- rows,
721
- help: hint,
722
- placeholder,
723
- wrapperClass: '$remove:formkit-wrapper',
724
- labelClass: 'ui-dynamic-form-input-label',
725
- inputClass: `input-${this.theme}`,
726
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
727
- readonly: isReadOnly,
728
- validation,
729
- validationVisibility: 'live',
730
- },
731
- };
732
- case 'time':
733
- return {
734
- type: 'FormKit',
735
- props: {
736
- type: 'time' /* with pro component mask more good */,
737
- id: field.id,
738
- name,
739
- label: field.label,
740
- required: field.required,
741
- value: this.formData[field.id],
742
- help: hint,
743
- placeholder,
744
- wrapperClass: '$remove:formkit-wrapper',
745
- labelClass: 'ui-dynamic-form-input-label',
746
- inputClass: `input-${this.theme}`,
747
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
748
- readonly: isReadOnly,
749
- validation,
750
- validationVisibility: 'live',
751
- },
752
- };
753
- case 'url':
754
- return {
755
- type: 'FormKit',
756
- props: {
757
- type: 'url',
758
- id: field.id,
759
- name,
760
- label: field.label,
761
- required: field.required,
762
- value: this.formData[field.id],
763
- help: hint,
764
- placeholder,
765
- wrapperClass: '$remove:formkit-wrapper',
766
- labelClass: 'ui-dynamic-form-input-label',
767
- inputClass: `input-${this.theme}`,
768
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
769
- readonly: isReadOnly,
770
- validation,
771
- validationVisibility: 'live',
772
- },
773
- };
774
- case 'week':
775
- return {
776
- type: 'FormKit',
777
- props: {
778
- type: 'week',
779
- id: field.id,
780
- name,
781
- label: field.label,
782
- required: field.required,
783
- value: this.formData[field.id],
784
- help: hint,
785
- placeholder,
786
- wrapperClass: '$remove:formkit-wrapper',
787
- labelClass: 'ui-dynamic-form-input-label',
788
- inputClass: `input-${this.theme}`,
789
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
790
- readonly: isReadOnly,
791
- validation,
792
- validationVisibility: 'live',
793
- },
794
- };
795
- default:
796
- return {
797
- type: 'FormKit',
798
- props: {
799
- type: field.type,
800
- id: field.id,
801
- name,
802
- label: field.label,
803
- required: field.required,
804
- value: this.formData[field.id],
805
- help: hint,
806
- labelClass: 'ui-dynamic-form-input-label',
807
- inputClass: `input-${this.theme}`,
808
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
809
- readonly: isReadOnly,
810
- validation,
811
- validationVisibility: 'live',
812
- },
813
- };
814
- }
815
- },
816
- toggleCollapse() {
817
- this.collapsed = !this.collapsed;
818
- },
819
- getRowWidthStyling(field, index) {
820
- let style = '';
821
- if (index === 0) {
822
- style += 'margin-top: 12px;';
823
- }
824
- if (field.type === 'header') {
825
- style += 'flex-basis: 100%;';
826
- } else {
827
- style += `flex-basis: 100%;`;
828
- }
829
- return style;
830
- },
831
- fields() {
832
- const aFields = this.userTask.userTaskConfig?.formFields ?? [];
833
- const fieldMap = aFields.map((field) => ({
834
- ...field,
835
- items: mapItems(field.type, field),
836
- }));
850
+ getRowWidthStyling(field, index) {
851
+ let style = '';
852
+ if (index === 0) {
853
+ style += 'margin-top: 12px;';
854
+ }
855
+ if (field.type === 'header') {
856
+ style += 'flex-basis: 100%;';
857
+ } else {
858
+ style += `flex-basis: 100%;`;
859
+ }
860
+ return style;
861
+ },
862
+ fields() {
863
+ const aFields = this.userTask.userTaskConfig?.formFields ?? [];
864
+ const fieldMap = aFields.map((field) => ({
865
+ ...field,
866
+ items: mapItems(field.type, field),
867
+ }));
837
868
 
838
- return fieldMap;
839
- },
840
- /*
841
- widget-action just sends a msg to Node-RED, it does not store the msg state server-side
842
- alternatively, you can use widget-change, which will also store the msg in the Node's datastore
843
- */
844
- send(msg, index) {
845
- const msgArr = [];
846
- msgArr[index] = msg;
847
- this.$socket.emit('widget-action', this.id, msgArr);
848
- },
849
- init(msg) {
850
- this.msg = msg;
851
- if (!msg) {
852
- return;
853
- }
869
+ return fieldMap;
870
+ },
871
+ send(msg, index) {
872
+ const msgArr = [];
873
+ msgArr[index] = msg;
874
+ this.$socket.emit('widget-action', this.id, msgArr);
875
+ },
876
+ init(msg) {
877
+ this.msg = msg;
878
+ if (!msg) {
879
+ return;
880
+ }
881
+
882
+ this.actions = this.props.options;
883
+
884
+ const hasTask = msg.payload && msg.payload.userTask;
885
+
886
+ if (hasTask) {
887
+ this.userTask = msg.payload.userTask;
888
+ } else {
889
+ this.userTask = null;
890
+ this.formData = {};
891
+ return;
892
+ }
854
893
 
855
- this.actions = this.props.options;
894
+ const formFields = this.userTask.userTaskConfig.formFields;
895
+ const formFieldIds = formFields.map((ff) => ff.id);
896
+ const initialValues = this.userTask.startToken;
897
+ const finishedFormData = msg.payload.formData;
898
+ this.formIsFinished = !!msg.payload.formData;
899
+ if (this.formIsFinished) {
900
+ this.collapsed = this.props.collapse_when_finished;
901
+ }
856
902
 
857
- const hasTask = msg.payload && msg.payload.userTask;
903
+ if (formFields) {
904
+ formFields.forEach((field) => {
905
+ this.formData[field.id] = field.defaultValue;
858
906
 
859
- if (hasTask) {
860
- this.userTask = msg.payload.userTask;
907
+ if (field.type === 'confirm') {
908
+ const customForm = field.customForm ? JSON.parse(JSON.stringify(field.customForm)) : {};
909
+ const confirmText = customForm.confirmButtonText ?? 'Confirm';
910
+ const declineText = customForm.declineButtonText ?? 'Decline';
911
+ const confirmActions = [
912
+ {
913
+ alignment: 'left',
914
+ primary: 'true',
915
+ label: confirmText,
916
+ condition: '',
917
+ isConfirmAction: true,
918
+ confirmFieldId: field.id,
919
+ confirmValue: true,
920
+ },
921
+ {
922
+ alignment: 'left',
923
+ primary: 'true',
924
+ label: declineText,
925
+ condition: '',
926
+ isConfirmAction: true,
927
+ confirmFieldId: field.id,
928
+ confirmValue: false,
929
+ },
930
+ ];
931
+ if (this.props.handle_confirmation_dialogs) {
932
+ this.actions = confirmActions;
861
933
  } else {
862
- this.userTask = null;
863
- this.formData = {};
864
- return;
934
+ this.actions = [...this.actions, ...confirmActions];
865
935
  }
936
+ }
937
+ });
938
+ }
866
939
 
867
- const formFields = this.userTask.userTaskConfig.formFields;
868
- const formFieldIds = formFields.map((ff) => ff.id);
869
- const initialValues = this.userTask.startToken;
870
- const finishedFormData = msg.payload.formData;
871
- this.formIsFinished = !!msg.payload.formData;
872
- if (this.formIsFinished) {
873
- this.collapsed = this.props.collapse_when_finished;
874
- }
940
+ if (initialValues) {
941
+ Object.keys(initialValues)
942
+ .filter((key) => formFieldIds.includes(key))
943
+ .forEach((key) => {
944
+ this.formData[key] = initialValues[key];
945
+ });
946
+ }
875
947
 
876
- if (formFields) {
877
- formFields.forEach((field) => {
878
- this.formData[field.id] = field.defaultValue;
948
+ if (this.formIsFinished) {
949
+ Object.keys(finishedFormData)
950
+ .filter((key) => formFieldIds.includes(key))
951
+ .forEach((key) => {
952
+ this.formData[key] = finishedFormData[key];
953
+ });
954
+ }
879
955
 
880
- if (field.type === 'confirm') {
881
- const customForm = field.customForm ? JSON.parse(field.customForm) : {};
882
- const confirmText = customForm.confirmButtonText ?? 'Confirm';
883
- const declineText = customForm.declineButtonText ?? 'Decline';
884
- this.actions = [
885
- {
886
- alignment: 'right',
887
- primary: 'false',
888
- label: declineText,
889
- condition: '',
890
- },
891
- {
892
- alignment: 'right',
893
- primary: 'true',
894
- label: confirmText,
895
- condition: '',
896
- },
897
- ];
898
- }
899
- });
900
- }
956
+ nextTick(() => {
957
+ this.focusFirstFormField();
958
+ });
959
+ },
960
+ actionFn(action) {
961
+ if (action.isTerminate) {
962
+ this.showError('');
901
963
 
902
- if (initialValues) {
903
- Object.keys(initialValues)
904
- .filter((key) => formFieldIds.includes(key))
905
- .forEach((key) => {
906
- this.formData[key] = initialValues[key];
907
- });
908
- }
964
+ const msg = this.msg ?? {};
965
+ msg.payload = {
966
+ formData: this.formData,
967
+ userTask: this.userTask,
968
+ isTerminate: true,
969
+ };
909
970
 
910
- if (this.formIsFinished) {
911
- Object.keys(finishedFormData)
912
- .filter((key) => formFieldIds.includes(key))
913
- .forEach((key) => {
914
- this.formData[key] = finishedFormData[key];
915
- });
916
- }
971
+ const terminateOutputIndex = this.totalOutputs - 1;
917
972
 
918
- nextTick(() => {
919
- this.focusFirstFormField();
920
- });
921
- },
922
- actionFn(action) {
923
- if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
924
- const formkitInputs = this.$refs.form.$el.querySelectorAll('.formkit-outer');
925
- let allComplete = true;
973
+ this.send(msg, terminateOutputIndex);
974
+ return;
975
+ }
926
976
 
927
- formkitInputs.forEach((input) => {
928
- const dataComplete = input.getAttribute('data-complete');
929
- const dataInvalid = input.getAttribute('data-invalid');
977
+ if (action.isSuspend) {
978
+ this.showError('');
930
979
 
931
- if (dataComplete == null && dataInvalid === 'true') {
932
- allComplete = false;
933
- }
934
- });
980
+ const msg = this.msg ?? {};
981
+ msg.payload = {
982
+ formData: this.formData,
983
+ userTask: this.userTask,
984
+ isSuspend: true,
985
+ };
935
986
 
936
- if (!allComplete) return;
937
- }
987
+ const suspendOutputIndex = this.totalOutputs - 2;
938
988
 
939
- if (this.checkCondition(action.condition)) {
940
- this.showError('');
989
+ this.send(msg, suspendOutputIndex);
990
+ return;
991
+ }
941
992
 
942
- const processedFormData = { ...this.formData };
943
- const formFields = this.userTask.userTaskConfig.formFields;
993
+ if (action.isConfirmAction && action.confirmFieldId) {
994
+ this.formData[action.confirmFieldId] = action.confirmValue;
995
+ }
944
996
 
945
- formFields.forEach((field) => {
946
- const fieldValue = processedFormData[field.id];
997
+ if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
998
+ const formkitInputs = this.$refs.form.$el.querySelectorAll('.formkit-outer');
999
+ let allComplete = true;
947
1000
 
948
- if (field.type === 'number' || field.type === 'long') {
949
- if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
950
- if (field.type === 'long') {
951
- const intValue = Number.parseInt(fieldValue, 10);
952
- if (!isNaN(intValue)) {
953
- processedFormData[field.id] = intValue;
954
- }
955
- } else {
956
- const numValue = Number.parseFloat(fieldValue);
957
- if (!isNaN(numValue)) {
958
- processedFormData[field.id] = numValue;
959
- }
960
- }
961
- }
962
- }
963
- });
1001
+ formkitInputs.forEach((input) => {
1002
+ const dataComplete = input.getAttribute('data-complete');
1003
+ const dataInvalid = input.getAttribute('data-invalid');
964
1004
 
965
- const msg = this.msg ?? {};
966
- msg.payload = { formData: processedFormData, userTask: this.userTask };
967
- this.send(
968
- msg,
969
- this.actions.findIndex((element) => element.label === action.label) +
970
- (this.isConfirmDialog ? this.props.options.length : 0)
971
- );
972
- // TODO: mm - end
973
- } else {
974
- this.showError(action.errorMessage);
975
- }
976
- },
977
- checkCondition(condition) {
978
- if (condition === '') return true;
979
- try {
980
- // eslint-disable-next-line no-new-func
981
- const func = Function('fields', 'userTask', 'msg', '"use strict"; return (' + condition + ')');
982
- const result = func(this.formData, this.userTask, this.msg);
983
- return Boolean(result);
984
- } catch (err) {
985
- console.error('Error while evaluating condition: ' + err);
986
- return false;
987
- }
988
- },
989
- showError(errMsg) {
990
- this.errorMsg = errMsg;
991
- },
992
- focusFirstFormField() {
993
- if (this.collapsed || !this.hasUserTask) {
994
- return;
995
- }
1005
+ if (dataComplete == null && dataInvalid === 'true') {
1006
+ allComplete = false;
1007
+ }
1008
+ });
996
1009
 
997
- if (this.firstFormFieldRef) {
998
- let inputElement = null;
1010
+ if (!allComplete) return;
1011
+ }
999
1012
 
1000
- if (this.firstFormFieldRef.node && this.firstFormFieldRef.node.input instanceof HTMLElement) {
1001
- inputElement = this.firstFormFieldRef.node.input;
1002
- } else if (this.firstFormFieldRef.$el instanceof HTMLElement) {
1003
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
1004
- inputElement = this.firstFormFieldRef.$el;
1005
- } else {
1006
- inputElement = this.firstFormFieldRef.$el.querySelector('input:not([type="hidden"]), textarea, select');
1007
- }
1008
- }
1013
+ if (this.checkCondition(action.condition)) {
1014
+ this.showError('');
1009
1015
 
1010
- if (inputElement) {
1011
- inputElement.focus();
1012
- } else {
1013
- console.warn('Could not find a focusable input element for the first form field.');
1016
+ const processedFormData = { ...this.formData };
1017
+ const formFields = this.userTask.userTaskConfig.formFields;
1018
+
1019
+ formFields.forEach((field) => {
1020
+ const fieldValue = processedFormData[field.id];
1021
+
1022
+ if (field.type === 'number' || field.type === 'long') {
1023
+ if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
1024
+ if (field.type === 'long') {
1025
+ const intValue = Number.parseInt(fieldValue, 10);
1026
+ if (!isNaN(intValue)) {
1027
+ processedFormData[field.id] = intValue;
1028
+ }
1029
+ } else {
1030
+ const numValue = Number.parseFloat(fieldValue);
1031
+ if (!isNaN(numValue)) {
1032
+ processedFormData[field.id] = numValue;
1014
1033
  }
1034
+ }
1015
1035
  }
1016
- },
1036
+ }
1037
+ });
1038
+
1039
+ const msg = this.msg ?? {};
1040
+ msg.payload = { formData: processedFormData, userTask: this.userTask };
1041
+ this.send(
1042
+ msg,
1043
+ this.actions.findIndex((element) => element.label === action.label) +
1044
+ (this.isConfirmDialog ? this.props.options.length : 0)
1045
+ );
1046
+ } else {
1047
+ this.showError(action.errorMessage);
1048
+ }
1049
+ },
1050
+ checkCondition(condition) {
1051
+ if (condition === '') return true;
1052
+ try {
1053
+ const func = Function('fields', 'userTask', 'msg', '"use strict"; return (' + condition + ')');
1054
+ const result = func(this.formData, this.userTask, this.msg);
1055
+ return Boolean(result);
1056
+ } catch (err) {
1057
+ console.error('Error while evaluating condition: ' + err);
1058
+ return false;
1059
+ }
1060
+ },
1061
+ showError(errMsg) {
1062
+ this.errorMsg = errMsg;
1063
+ },
1064
+ focusFirstFormField() {
1065
+ if (this.collapsed || !this.hasUserTask) {
1066
+ return;
1067
+ }
1068
+
1069
+ if (this.firstFormFieldRef) {
1070
+ let inputElement = null;
1071
+
1072
+ if (this.firstFormFieldRef.node && this.firstFormFieldRef.node.input instanceof HTMLElement) {
1073
+ inputElement = this.firstFormFieldRef.node.input;
1074
+ } else if (this.firstFormFieldRef.$el instanceof HTMLElement) {
1075
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
1076
+ inputElement = this.firstFormFieldRef.$el;
1077
+ } else {
1078
+ inputElement = this.firstFormFieldRef.$el.querySelector('input:not([type="hidden"]), textarea, select');
1079
+ }
1080
+ }
1081
+
1082
+ if (inputElement) {
1083
+ inputElement.focus();
1084
+ } else {
1085
+ console.warn('Could not find a focusable input element for the first form field.');
1086
+ }
1087
+ }
1017
1088
  },
1089
+ },
1018
1090
  };
1019
1091
 
1020
1092
  function mapItems(type, field) {
1021
- if (type === 'enum') {
1022
- return field.enumValues.map((enumValue) => ({
1023
- title: enumValue.name,
1024
- value: enumValue.id,
1025
- }));
1026
- } else {
1027
- return null;
1028
- }
1093
+ if (type === 'enum') {
1094
+ return field.enumValues.map((enumValue) => ({
1095
+ title: enumValue.name,
1096
+ value: enumValue.id,
1097
+ }));
1098
+ } else {
1099
+ return null;
1100
+ }
1029
1101
  }
1030
1102
  </script>
1031
1103