@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-file-preview-1bee64-mdpzqcz2 → 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,8 +1,20 @@
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
5
- v-if="props.title_style === 'outside' && hasUserTask"
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'"
6
18
  :style="props.title_style"
7
19
  :title="effectiveTitle"
8
20
  :customStyles="props.title_custom_text_styling"
@@ -10,119 +22,106 @@
10
22
  :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
11
23
  :collapsed="collapsed"
12
24
  :toggleCollapse="toggleCollapse"
13
- />
14
- <div className="ui-dynamic-form-wrapper">
15
- <p v-if="hasUserTask" style="margin-bottom: 0px">
16
- <v-form ref="form" v-model="form" :class="dynamicClass">
17
- <UIDynamicFormTitleText
18
- v-if="props.title_style != 'outside'"
19
- :style="props.title_style"
20
- :title="effectiveTitle"
21
- :customStyles="props.title_custom_text_styling"
22
- :titleIcon="props.title_icon"
23
- :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
24
- :collapsed="collapsed"
25
- :toggleCollapse="toggleCollapse"
26
- />
27
- <Transition name="cardCollapse">
28
- <div v-if="!collapsed">
29
- <div
30
- className="ui-dynamic-form-formfield-positioner"
31
- :style="props.inner_card_styling"
32
- :data-columns="props.form_columns || 1"
33
- >
34
- <FormKit id="form" type="group">
35
- <v-row
36
- v-for="(field, index) in fields()"
37
- :key="field"
38
- :class="field.type === 'header' ? 'ui-dynamic-form-header-row' : ''"
39
- :style="getRowWidthStyling(field, index)"
40
- >
41
- <v-col cols="12">
42
- <component
43
- :is="getFieldComponent(field).type"
44
- v-if="getFieldComponent(field).innerHTML"
45
- v-bind="getFieldComponent(field).props"
46
- :class="getFieldComponent(field).class"
47
- v-html="getFieldComponent(field).innerHTML"
48
- :ref="
49
- (el) => {
50
- if (index === 0) firstFormFieldRef = el;
51
- }
52
- "
53
- />
54
- <component
55
- :is="getFieldComponent(field).type"
56
- v-else-if="getFieldComponent(field).innerText"
57
- v-bind="getFieldComponent(field).props"
58
- :ref="
59
- (el) => {
60
- if (index === 0) firstFormFieldRef = el;
61
- }
62
- "
63
- v-model="formData[field.id]"
64
- >
65
- {{ getFieldComponent(field).innerText }}
66
- </component>
67
- <div v-else-if="getFieldComponent(field).type == 'v-slider'">
68
- <p class="formkit-label">{{ field.label }}</p>
69
- <component
70
- :is="getFieldComponent(field).type"
71
- v-bind="getFieldComponent(field).props"
72
- :ref="
73
- (el) => {
74
- if (index === 0) firstFormFieldRef = el;
75
- }
76
- "
77
- v-model="field.defaultValue"
78
- />
79
- <p class="formkit-help">
80
- {{ getFieldHint(field) }}
81
- </p>
82
- </div>
83
- <component
84
- :is="getFieldComponent(field).type"
85
- v-else
86
- v-bind="getFieldComponent(field).props"
87
- :ref="
88
- (el) => {
89
- if (index === 0) firstFormFieldRef = el;
90
- }
91
- "
92
- v-model="formData[field.id]"
93
- />
94
- </v-col>
95
- </v-row>
96
- </FormKit>
97
- </div>
98
- <v-row :class="dynamicFooterClass">
99
- <v-row v-if="errorMsg.length > 0" style="padding: 12px">
100
- <v-alert type="error">Error: {{ errorMsg }}</v-alert>
101
- </v-row>
102
- <UIDynamicFormFooterAction
103
- v-if="props.actions_inside_card && actions.length > 0"
104
- :actions="actions"
105
- :actionCallback="actionFn"
106
- :formIsFinished="formIsFinished"
107
- style="padding: 16px; padding-top: 0px"
108
- />
109
- </v-row>
110
- </div>
111
- </Transition>
112
- </v-form>
113
- </p>
114
- <p v-else>
115
- <v-alert
116
- v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
117
- :text="props.waiting_info"
118
- :title="props.waiting_title"
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"
119
107
  />
120
- </p>
121
- </div>
122
- <div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px">
123
- <UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
124
- </div>
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>
125
120
  </div>
121
+ <div v-if="!props.actions_inside_card && hasUserTask && actions.length > 0" style="padding-top: 32px">
122
+ <UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
123
+ </div>
124
+ </div>
126
125
  </template>
127
126
 
128
127
  <script>
@@ -137,1440 +136,972 @@ import UIDynamicFormFooterAction from './FooterActions.vue';
137
136
  import UIDynamicFormTitleText from './TitleText.vue';
138
137
 
139
138
  function requiredIf({ value }, [targetField, expectedValue], node) {
140
- const actual = node?.root?.value?.[targetField];
141
- const isEmpty = value === '' || value === null || value === undefined;
139
+ console.debug(arguments);
142
140
 
143
- if (actual === expectedValue && isEmpty) {
144
- return false;
145
- }
141
+ const actual = node?.root?.value?.[targetField];
142
+ const isEmpty = value === '' || value === null || value === undefined;
146
143
 
147
- return true;
144
+ if (actual === expectedValue && isEmpty) {
145
+ return false;
146
+ }
147
+
148
+ return true;
148
149
  }
149
150
 
150
151
  class MarkdownRenderer extends marked.Renderer {
151
- link(params) {
152
- const link = super.link(params);
153
- return link.replace('<a', "<a target='_blank'");
154
- }
155
-
156
- html(params) {
157
- const result = super.html(params);
158
- if (result.startsWith('<a ') && !result.includes('target=')) {
159
- return result.replace('<a ', `<a target="_blank" `);
160
- }
161
- return result;
152
+ link(params) {
153
+ const link = super.link(params);
154
+ return link.replace('<a', "<a target='_blank'");
155
+ }
156
+
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" `);
162
161
  }
162
+ return result;
163
+ }
163
164
  }
164
165
 
165
166
  class MarkedHooks extends marked.Hooks {
166
- postprocess(html) {
167
- return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
168
- }
167
+ postprocess(html) {
168
+ return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
169
+ }
169
170
  }
170
171
 
171
172
  function processMarkdown(content) {
172
- if (!content) return '';
173
+ if (!content) return '';
173
174
 
174
- const html = marked.parse(content.toString(), {
175
- renderer: new MarkdownRenderer(),
176
- hooks: new MarkedHooks(),
177
- });
175
+ const html = marked.parse(content.toString(), {
176
+ renderer: new MarkdownRenderer(),
177
+ hooks: new MarkedHooks(),
178
+ });
178
179
 
179
- return html;
180
+ return html;
180
181
  }
181
182
 
182
183
  export default {
183
- name: 'UIDynamicForm',
184
- components: {
185
- FormKit,
186
- UIDynamicFormFooterAction,
187
- UIDynamicFormTitleText,
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 }),
188
197
  },
189
- inject: ['$socket'],
190
- props: {
191
- /* do not remove entries from this - Dashboard's Layout Manager's will pass this data to your component */
192
- id: { type: String, required: true },
193
- props: { type: Object, default: () => ({}) },
194
- state: {
195
- type: Object,
196
- default: () => ({ enabled: false, visible: false }),
197
- },
198
+ },
199
+ setup(props) {
200
+ console.info('UIDynamicForm setup with:', props);
201
+ console.debug('Vue function loaded correctly', markRaw);
202
+
203
+ const instance = getCurrentInstance();
204
+ const app = instance.appContext.app;
205
+
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`;
198
230
  },
199
- setup(props) {
200
- const instance = getCurrentInstance();
201
- const app = instance.appContext.app;
202
-
203
- const formkitConfig = defaultConfig({
204
- theme: 'genesis',
205
- locales: { de },
206
- locale: 'de',
207
- rules: { requiredIf: requiredIf },
208
- });
209
- app.use(plugin, formkitConfig);
231
+ dynamicFooterClass() {
232
+ return `ui-dynamic-form-footer-${this.theme} ui-dynamic-form-footer-common`;
210
233
  },
211
- data() {
212
- return {
213
- actions: [],
214
- formData: {},
215
- userTask: null,
216
- theme: '',
217
- errorMsg: '',
218
- formIsFinished: false,
219
- msg: null,
220
- collapsed: false,
221
- firstFormFieldRef: null,
222
- intersectionObserver: null,
223
- visibleFileFields: new Set(),
224
- };
234
+ hasUserTask() {
235
+ return !!this.userTask;
225
236
  },
226
- computed: {
227
- dynamicClass() {
228
- return `ui-dynamic-form-${this.theme} ui-dynamic-form-common`;
229
- },
230
- dynamicFooterClass() {
231
- return `ui-dynamic-form-footer-${this.theme} ui-dynamic-form-footer-common`;
232
- },
233
- hasUserTask() {
234
- return !!this.userTask;
235
- },
236
- totalOutputs() {
237
- return (
238
- this.props.options.length +
239
- (this.props.handle_confirmation_dialogs ? 2 : 0) +
240
- (this.props.trigger_on_change ? 1 : 0)
241
- );
242
- },
243
- isConfirmDialog() {
244
- return this.userTask?.userTaskConfig?.formFields?.some((field) => field.type === 'confirm') || false;
245
- },
246
- effectiveTitle() {
247
- if (this.props.title_text_type === 'str') {
248
- return this.props.title_text;
249
- } else if (this.props.title_text_type === 'msg') {
250
- return this.msg.dynamicTitle;
251
- } else {
252
- return '';
253
- }
254
- },
255
- // Optimized computed property for field components
256
- fieldComponents() {
257
- if (!this.userTask?.userTaskConfig?.formFields) {
258
- return {};
259
- }
260
-
261
- const components = {};
262
- const aFields = this.userTask.userTaskConfig.formFields;
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,
282
+ },
283
+ },
284
+ created() {
285
+ const currentPath = window.location.pathname;
286
+ const lastPart = currentPath.substring(currentPath.lastIndexOf('/'));
287
+
288
+ const store = this.$store.state;
289
+
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';
299
+ }
300
+ break;
301
+ }
302
+ }
303
+ },
304
+ mounted() {
305
+ const elements = document.querySelectorAll('.formkit-input');
263
306
 
264
- aFields.forEach((field) => {
265
- components[field.id] = this.createComponent(field);
266
- });
307
+ elements.forEach((element) => {
308
+ element.classList.add('test');
309
+ });
267
310
 
268
- return components;
269
- },
270
- // Optimized computed property for fields
271
- computedFields() {
272
- const aFields = this.userTask?.userTaskConfig?.formFields ?? [];
273
- return aFields.map((field) => ({
274
- ...field,
275
- items: mapItems(field.type, field),
276
- }));
277
- },
278
- },
279
- watch: {
280
- formData: {
281
- handler(newData, oldData) {
282
- if (this.props.trigger_on_change) {
283
- const res = { payload: { formData: newData, userTask: this.userTask } };
284
- this.send(res, this.totalOutputs - 1);
285
- }
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',
286
358
  },
287
- collapsed(newVal) {
288
- if (!newVal && this.hasUserTask) {
289
- nextTick(() => {
290
- this.focusFirstFormField();
291
- });
292
- }
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',
293
381
  },
294
- userTask(newVal) {
295
- if (newVal && !this.collapsed) {
296
- nextTick(() => {
297
- this.focusFirstFormField();
298
- });
299
- }
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',
300
546
  },
301
- deep: true,
302
- },
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
+ }
303
846
  },
304
- created() {
305
- const currentPath = window.location.pathname;
306
- const lastPart = currentPath.substring(currentPath.lastIndexOf('/'));
307
-
308
- const store = this.$store.state;
309
-
310
- for (const key in store.ui.pages) {
311
- if (store.ui.pages[key].path === lastPart) {
312
- const theme = store.ui.pages[key].theme;
313
- if (store.ui.themes[theme].name === 'ProcessCube Lightmode') {
314
- this.theme = 'light';
315
- } else if (store.ui.themes[theme].name === 'ProcessCube Darkmode') {
316
- this.theme = 'dark';
317
- } else {
318
- this.theme = 'default';
319
- }
320
- break;
321
- }
322
- }
847
+ toggleCollapse() {
848
+ this.collapsed = !this.collapsed;
323
849
  },
324
- mounted() {
325
- const elements = document.querySelectorAll('.formkit-input');
326
-
327
- elements.forEach((element) => {
328
- element.classList.add('test');
329
- });
330
-
331
- // Initialize Intersection Observer for lazy loading
332
- this.initLazyLoading();
333
-
334
- this.$socket.on('widget-load:' + this.id, (msg) => {
335
- this.init(msg);
336
- });
337
- this.$socket.on('msg-input:' + this.id, (msg) => {
338
- // store the latest message in our client-side vuex store when we receive a new message
339
- this.init(msg);
340
- });
341
- // tell Node-RED that we're loading a new instance of this widget
342
- this.$socket.emit('widget-load', this.id);
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;
343
861
  },
344
- unmounted() {
345
- /* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
346
- this.$socket?.off('widget-load' + this.id);
347
- this.$socket?.off('msg-input:' + this.id);
348
-
349
- // Clean up Intersection Observer
350
- if (this.intersectionObserver) {
351
- this.intersectionObserver.disconnect();
352
- }
862
+ fields() {
863
+ const aFields = this.userTask.userTaskConfig?.formFields ?? [];
864
+ const fieldMap = aFields.map((field) => ({
865
+ ...field,
866
+ items: mapItems(field.type, field),
867
+ }));
868
+
869
+ return fieldMap;
353
870
  },
354
- methods: {
355
- // Simplified component getter - now just returns from computed cache
356
- getFieldComponent(field) {
357
- return this.fieldComponents[field.id] || this.createComponent(field);
358
- },
359
-
360
- // Clear cache when form data changes
361
- clearComponentCache() {
362
- // This is now handled by computed properties automatically
363
- },
364
-
365
- // Safe method to get field hint for template use
366
- getFieldHint(field) {
367
- try {
368
- if (field.customForm) {
369
- let customForm;
370
- if (typeof field.customForm === 'string') {
371
- customForm = JSON.parse(field.customForm);
372
- } else if (typeof field.customForm === 'object') {
373
- customForm = field.customForm;
374
- }
375
- return customForm?.hint;
376
- }
377
- } catch (error) {
378
- console.warn('Failed to parse customForm hint for field', field.id, error);
379
- }
380
- return undefined;
381
- },
382
-
383
- createComponent(field) {
384
- // Safe parsing of customForm - handle both string and object cases
385
- let customForm = {};
386
- try {
387
- if (field.customForm) {
388
- if (typeof field.customForm === 'string') {
389
- customForm = JSON.parse(field.customForm);
390
- } else if (typeof field.customForm === 'object') {
391
- customForm = field.customForm;
392
- }
393
- }
394
- } catch (error) {
395
- console.warn('Failed to parse customForm for field', field.id, error);
396
- customForm = {};
397
- }
398
- const { hint, placeholder, validation, customProperties = [] } = customForm;
399
- const name = field.id;
400
- const isReadOnly =
401
- this.props.readonly ||
402
- this.formIsFinished ||
403
- customProperties.some(
404
- (entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'
405
- )
406
- ? 'true'
407
- : undefined;
408
-
409
- const commonFormKitProps = {
410
- id: field.id,
411
- name,
412
- label: field.label,
413
- required: field.required,
414
- value: this.formData[field.id],
415
- help: hint,
416
- wrapperClass: '$remove:formkit-wrapper',
417
- labelClass: 'ui-dynamic-form-input-label',
418
- inputClass: `input-${this.theme}`,
419
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
420
- readonly: isReadOnly,
421
- validation,
422
- validationVisibility: 'live',
423
- };
424
-
425
- switch (field.type) {
426
- case 'long':
427
- return {
428
- type: 'FormKit',
429
- props: {
430
- ...commonFormKitProps,
431
- type: 'number',
432
- number: 'integer',
433
- min: 0,
434
- validation: validation ? `${validation}|number` : 'number',
435
- },
436
- };
437
- case 'number':
438
- const step = customForm.step;
439
- return {
440
- type: 'FormKit',
441
- props: {
442
- ...commonFormKitProps,
443
- type: 'number',
444
- step,
445
- number: 'float',
446
- validation: validation ? `${validation}|number` : 'number',
447
- },
448
- };
449
- case 'date':
450
- return {
451
- type: 'FormKit',
452
- props: {
453
- ...commonFormKitProps,
454
- type: 'date',
455
- },
456
- };
457
- case 'string':
458
- return {
459
- type: 'FormKit',
460
- props: {
461
- ...commonFormKitProps,
462
- type: 'text',
463
- placeholder,
464
- },
465
- };
466
- case 'email':
467
- return {
468
- type: 'FormKit',
469
- props: {
470
- ...commonFormKitProps,
471
- type: 'email',
472
- placeholder,
473
- },
474
- };
475
- case 'password':
476
- return {
477
- type: 'FormKit',
478
- props: {
479
- ...commonFormKitProps,
480
- type: 'password',
481
- placeholder,
482
- },
483
- };
484
- case 'tel':
485
- return {
486
- type: 'FormKit',
487
- props: {
488
- ...commonFormKitProps,
489
- type: 'tel',
490
- placeholder,
491
- },
492
- };
493
- case 'url':
494
- return {
495
- type: 'FormKit',
496
- props: {
497
- ...commonFormKitProps,
498
- type: 'url',
499
- placeholder,
500
- },
501
- };
502
- case 'time':
503
- return {
504
- type: 'FormKit',
505
- props: {
506
- ...commonFormKitProps,
507
- type: 'time',
508
- placeholder,
509
- },
510
- };
511
- case 'week':
512
- return {
513
- type: 'FormKit',
514
- props: {
515
- ...commonFormKitProps,
516
- type: 'week',
517
- placeholder,
518
- },
519
- };
520
- case 'month':
521
- return {
522
- type: 'FormKit',
523
- props: {
524
- ...commonFormKitProps,
525
- type: 'month',
526
- },
527
- };
528
- case 'datetime-local':
529
- return {
530
- type: 'FormKit',
531
- props: {
532
- ...commonFormKitProps,
533
- type: 'datetime-local',
534
- },
535
- };
536
- case 'enum':
537
- const enums = field.enumValues.map((obj) => {
538
- return { value: obj.id, label: obj.name };
539
- });
540
- return {
541
- type: 'FormKit',
542
- props: {
543
- type: 'select', // JSON.parse(field.customForm).displayAs
544
- id: field.id,
545
- name,
546
- label: field.label,
547
- required: field.required,
548
- value: this.formData[field.id],
549
- options: enums,
550
- help: hint,
551
- wrapperClass: '$remove:formkit-wrapper',
552
- labelClass: 'ui-dynamic-form-input-label',
553
- inputClass: `input-${this.theme}`,
554
- innerClass: `ui-dynamic-form-input-outlines ${
555
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
556
- }`,
557
- readonly: isReadOnly,
558
- disabled: isReadOnly,
559
- validation,
560
- validationVisibility: 'live',
561
- },
562
- };
563
- case 'select':
564
- const selections = customForm.entries
565
- ? customForm.entries.map((obj) => {
566
- return { value: obj.key, label: obj.value };
567
- })
568
- : [];
569
- return {
570
- type: 'FormKit',
571
- props: {
572
- type: 'select', // JSON.parse(field.customForm).displayAs
573
- id: field.id,
574
- name,
575
- label: field.label,
576
- required: field.required,
577
- value: this.formData[field.id],
578
- options: selections,
579
- placeholder,
580
- help: hint,
581
- wrapperClass: '$remove:formkit-wrapper',
582
- labelClass: 'ui-dynamic-form-input-label',
583
- inputClass: `input-${this.theme}`,
584
- innerClass: `ui-dynamic-form-input-outlines ${
585
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
586
- }`,
587
- readonly: isReadOnly,
588
- disabled: isReadOnly,
589
- validation,
590
- validationVisibility: 'live',
591
- },
592
- };
593
- case 'string':
594
- return {
595
- type: 'FormKit',
596
- props: {
597
- type: 'text',
598
- id: field.id,
599
- name,
600
- label: field.label,
601
- required: field.required,
602
- value: this.formData[field.id],
603
- help: hint,
604
- placeholder,
605
- wrapperClass: '$remove:formkit-wrapper',
606
- labelClass: 'ui-dynamic-form-input-label',
607
- inputClass: `input-${this.theme}`,
608
- innerClass: `ui-dynamic-form-input-outlines ${
609
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
610
- }`,
611
- readonly: isReadOnly,
612
- validation,
613
- validationVisibility: 'live',
614
- },
615
- };
616
- case 'confirm':
617
- return {
618
- type: 'h3',
619
- innerText: field.label,
620
- };
621
- case 'boolean':
622
- return {
623
- type: 'FormKit',
624
- props: {
625
- type: 'checkbox',
626
- id: field.id,
627
- name,
628
- label: field.label,
629
- required: field.required,
630
- value: this.formData[field.id],
631
- help: hint,
632
- labelClass: 'ui-dynamic-form-input-label',
633
- inputClass: `input-${this.theme}`,
634
- innerClass: `ui-dynamic-form-input-outlines ${
635
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
636
- }`,
637
- readonly: isReadOnly,
638
- disabled: isReadOnly,
639
- validation,
640
- validationVisibility: 'live',
641
- },
642
- };
643
- case 'file-preview':
644
- // Handle file preview display only (no upload functionality)
645
- const originalFieldId = field.id.replace('_preview', '');
646
- if (this.formData && this.formData[originalFieldId] && this.formData[originalFieldId].length != 0) {
647
- const fileDataArray = Array.isArray(this.formData[originalFieldId])
648
- ? this.formData[originalFieldId]
649
- : [this.formData[originalFieldId]];
650
-
651
- // Create unique container ID for this field
652
- const containerId = `file-preview-${field.id}`;
653
-
654
- // Check if this field is already visible (for immediate processing)
655
- if (this.visibleFileFields.has(field.id)) {
656
- // Return loading state initially
657
- const loadingContent = `
658
- <div id="${containerId}" data-lazy-field="${field.id}">
659
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
660
- field.required ? ' *' : ''
661
- }</label>
662
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
663
- <div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
664
- <div>Dateien werden geladen...</div>
665
- </div>
666
- </div>
667
- `;
668
-
669
- // Process files asynchronously
670
- setTimeout(() => {
671
- this.processFilePreview(containerId, fileDataArray, field);
672
- }, 0);
673
-
674
- return {
675
- type: 'div',
676
- props: {
677
- innerHTML: loadingContent,
678
- },
679
- };
680
- } else {
681
- // Return lazy loading placeholder
682
- const lazyContent = `
683
- <div id="${containerId}" data-lazy-field="${field.id}" class="lazy-file-preview">
684
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
685
- field.required ? ' *' : ''
686
- }</label>
687
- <div style="margin-top: 8px; padding: 40px; text-align: center; color: #999; border: 1px dashed #ddd; border-radius: 4px;">
688
- <div style="font-size: 1.5em; margin-bottom: 12px;">📁</div>
689
- <div>Dateien werden geladen, wenn sie sichtbar werden...</div>
690
- <div style="margin-top: 8px; font-size: 0.9em;">${
691
- fileDataArray.length
692
- } Datei(en)</div>
693
- </div>
694
- </div>
695
- `;
696
-
697
- return {
698
- type: 'div',
699
- props: {
700
- innerHTML: lazyContent,
701
- },
702
- };
703
- }
704
- }
705
- // If no files to preview, return empty div
706
- return {
707
- type: 'div',
708
- props: {
709
- style: 'display: none;',
710
- },
711
- };
712
- case 'file':
713
- const multiple = customForm.multiple === 'true';
714
- return {
715
- type: 'FormKit',
716
- props: {
717
- type: 'file',
718
- id: field.id,
719
- name,
720
- label: field.label,
721
- required: field.required,
722
- help: hint,
723
- innerClass: 'reset-background',
724
- wrapperClass: '$remove:formkit-wrapper',
725
- labelClass: 'ui-dynamic-form-input-label',
726
- inputClass: `input-${this.theme}`,
727
- readonly: isReadOnly,
728
- disabled: isReadOnly,
729
- multiple,
730
- validation,
731
- validationVisibility: 'live',
732
- },
733
- };
734
- case 'checkbox':
735
- const options = customForm.entries
736
- ? customForm.entries.map((obj) => {
737
- return { value: obj.key, label: obj.value };
738
- })
739
- : [];
740
- return {
741
- type: 'FormKit',
742
- props: {
743
- type: 'checkbox',
744
- id: field.id,
745
- name,
746
- label: field.label,
747
- required: field.required,
748
- value: this.formData[field.id],
749
- options,
750
- help: hint,
751
- fieldsetClass: 'custom-fieldset',
752
- labelClass: 'ui-dynamic-form-input-label',
753
- inputClass: `input-${this.theme}`,
754
- innerClass: `ui-dynamic-form-input-outlines ${
755
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
756
- }`,
757
- readonly: isReadOnly,
758
- disabled: isReadOnly,
759
- validation,
760
- validationVisibility: 'live',
761
- },
762
- };
763
- case 'color':
764
- return {
765
- type: 'FormKit',
766
- props: {
767
- type: 'color',
768
- id: field.id,
769
- name,
770
- label: field.label,
771
- required: field.required,
772
- value: this.formData[field.id],
773
- help: hint,
774
- readonly: isReadOnly,
775
- disabled: isReadOnly,
776
- validation,
777
- validationVisibility: 'live',
778
- },
779
- };
780
- case 'datetime-local':
781
- return {
782
- type: 'FormKit',
783
- props: {
784
- type: 'datetime-local',
785
- id: field.id,
786
- name,
787
- label: field.label,
788
- required: field.required,
789
- value: this.formData[field.id],
790
- help: hint,
791
- wrapperClass: '$remove:formkit-wrapper',
792
- labelClass: 'ui-dynamic-form-input-label',
793
- inputClass: `input-${this.theme}`,
794
- innerClass: `ui-dynamic-form-input-outlines ${
795
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
796
- }`,
797
- readonly: isReadOnly,
798
- validation,
799
- validationVisibility: 'live',
800
- },
801
- };
802
- case 'email':
803
- return {
804
- type: 'FormKit',
805
- props: {
806
- type: 'email',
807
- id: field.id,
808
- name,
809
- label: field.label,
810
- required: field.required,
811
- value: this.formData[field.id],
812
- help: hint,
813
- placeholder,
814
- wrapperClass: '$remove:formkit-wrapper',
815
- labelClass: 'ui-dynamic-form-input-label',
816
- inputClass: `input-${this.theme}`,
817
- innerClass: `ui-dynamic-form-input-outlines ${
818
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
819
- }`,
820
- readonly: isReadOnly,
821
- validation,
822
- validationVisibility: 'live',
823
- },
824
- };
825
- case 'header':
826
- let typeToUse = 'h1';
827
- if (customForm.style === 'heading_2') {
828
- typeToUse = 'h2';
829
- }
830
- if (customForm.style === 'heading_3') {
831
- typeToUse = 'h3';
832
- }
833
- return {
834
- type: typeToUse,
835
- innerText: this.formData[field.id],
836
- };
837
- case 'hidden':
838
- return {
839
- type: 'input',
840
- props: {
841
- type: 'hidden',
842
- value: this.formData[field.id],
843
- },
844
- };
845
- case 'month':
846
- return {
847
- type: 'FormKit',
848
- props: {
849
- type: 'month',
850
- id: field.id,
851
- name,
852
- label: field.label,
853
- required: field.required,
854
- value: this.formData[field.id],
855
- help: hint,
856
- wrapperClass: '$remove:formkit-wrapper',
857
- labelClass: 'ui-dynamic-form-input-label',
858
- inputClass: `input-${this.theme}`,
859
- innerClass: `ui-dynamic-form-input-outlines ${
860
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
861
- }`,
862
- readonly: isReadOnly,
863
- validation,
864
- validationVisibility: 'live',
865
- },
866
- };
867
- case 'paragraph':
868
- const paragraphContent = this.formData[field.id] || field.defaultValue || field.label || '';
869
- const processedHtml = processMarkdown(paragraphContent);
870
- return {
871
- type: 'div',
872
- innerHTML: processedHtml,
873
- class: 'ui-dynamic-form-paragraph',
874
- };
875
- case 'password':
876
- return {
877
- type: 'FormKit',
878
- props: {
879
- type: 'password',
880
- id: field.id,
881
- name,
882
- label: field.label,
883
- required: field.required,
884
- value: this.formData[field.id],
885
- help: hint,
886
- placeholder,
887
- wrapperClass: '$remove:formkit-wrapper',
888
- labelClass: 'ui-dynamic-form-input-label',
889
- inputClass: `input-${this.theme}`,
890
- innerClass: `ui-dynamic-form-input-outlines ${
891
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
892
- }`,
893
- readonly: isReadOnly,
894
- validation,
895
- validationVisibility: 'live',
896
- },
897
- };
898
- case 'radio':
899
- const radioOptions = customForm.entries
900
- ? customForm.entries.map((obj) => {
901
- return { value: obj.key, label: obj.value };
902
- })
903
- : [];
904
- return {
905
- type: 'FormKit',
906
- props: {
907
- type: 'radio',
908
- id: field.id,
909
- name,
910
- label: field.label,
911
- required: field.required,
912
- value: this.formData[field.id],
913
- options: radioOptions,
914
- help: hint,
915
- fieldsetClass: 'custom-fieldset',
916
- labelClass: 'ui-dynamic-form-input-label',
917
- inputClass: `input-${this.theme}`,
918
- innerClass: `ui-dynamic-form-input-outlines ${
919
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
920
- }`,
921
- readonly: isReadOnly,
922
- disabled: isReadOnly,
923
- validation,
924
- validationVisibility: 'live',
925
- },
926
- };
927
- case 'range':
928
- return {
929
- type: 'v-slider',
930
- props: {
931
- id: field.id,
932
- name,
933
- // label: field.label,
934
- required: field.required,
935
- // value: this.formData[field.id],
936
- // help: hint,
937
- min: customForm.min,
938
- max: customForm.max,
939
- step: customForm.step,
940
- thumbLabel: true,
941
- // wrapperClass: '$remove:formkit-wrapper',
942
- labelClass: 'ui-dynamic-form-input-label',
943
- // inputClass: `input-${this.theme}`,
944
- // innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
945
- readonly: isReadOnly,
946
- disabled: isReadOnly,
947
- validation,
948
- validationVisibility: 'live',
949
- },
950
- };
951
- case 'tel':
952
- return {
953
- type: 'FormKit',
954
- props: {
955
- type: 'tel' /* with pro component mask more good */,
956
- id: field.id,
957
- name,
958
- label: field.label,
959
- required: field.required,
960
- value: this.formData[field.id],
961
- help: hint,
962
- placeholder,
963
- wrapperClass: '$remove:formkit-wrapper',
964
- labelClass: 'ui-dynamic-form-input-label',
965
- inputClass: `input-${this.theme}`,
966
- innerClass: `ui-dynamic-form-input-outlines ${
967
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
968
- }`,
969
- readonly: isReadOnly,
970
- validation,
971
- validationVisibility: 'live',
972
- },
973
- };
974
- case 'textarea':
975
- const rows = customForm.rows;
976
- return {
977
- type: 'FormKit',
978
- props: {
979
- type: 'textarea' /* with pro component mask more good */,
980
- id: field.id,
981
- name,
982
- label: field.label,
983
- required: field.required,
984
- value: this.formData[field.id],
985
- rows,
986
- help: hint,
987
- placeholder,
988
- wrapperClass: '$remove:formkit-wrapper',
989
- labelClass: 'ui-dynamic-form-input-label',
990
- inputClass: `input-${this.theme}`,
991
- innerClass: `ui-dynamic-form-input-outlines ${
992
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
993
- }`,
994
- readonly: isReadOnly,
995
- validation,
996
- validationVisibility: 'live',
997
- },
998
- };
999
- case 'time':
1000
- return {
1001
- type: 'FormKit',
1002
- props: {
1003
- type: 'time' /* with pro component mask more good */,
1004
- id: field.id,
1005
- name,
1006
- label: field.label,
1007
- required: field.required,
1008
- value: this.formData[field.id],
1009
- help: hint,
1010
- placeholder,
1011
- wrapperClass: '$remove:formkit-wrapper',
1012
- labelClass: 'ui-dynamic-form-input-label',
1013
- inputClass: `input-${this.theme}`,
1014
- innerClass: `ui-dynamic-form-input-outlines ${
1015
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1016
- }`,
1017
- readonly: isReadOnly,
1018
- validation,
1019
- validationVisibility: 'live',
1020
- },
1021
- };
1022
- case 'url':
1023
- return {
1024
- type: 'FormKit',
1025
- props: {
1026
- type: 'url',
1027
- id: field.id,
1028
- name,
1029
- label: field.label,
1030
- required: field.required,
1031
- value: this.formData[field.id],
1032
- help: hint,
1033
- placeholder,
1034
- wrapperClass: '$remove:formkit-wrapper',
1035
- labelClass: 'ui-dynamic-form-input-label',
1036
- inputClass: `input-${this.theme}`,
1037
- innerClass: `ui-dynamic-form-input-outlines ${
1038
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1039
- }`,
1040
- readonly: isReadOnly,
1041
- validation,
1042
- validationVisibility: 'live',
1043
- },
1044
- };
1045
- case 'week':
1046
- return {
1047
- type: 'FormKit',
1048
- props: {
1049
- type: 'week',
1050
- id: field.id,
1051
- name,
1052
- label: field.label,
1053
- required: field.required,
1054
- value: this.formData[field.id],
1055
- help: hint,
1056
- placeholder,
1057
- wrapperClass: '$remove:formkit-wrapper',
1058
- labelClass: 'ui-dynamic-form-input-label',
1059
- inputClass: `input-${this.theme}`,
1060
- innerClass: `ui-dynamic-form-input-outlines ${
1061
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1062
- }`,
1063
- readonly: isReadOnly,
1064
- validation,
1065
- validationVisibility: 'live',
1066
- },
1067
- };
1068
- default:
1069
- return {
1070
- type: 'FormKit',
1071
- props: {
1072
- type: field.type,
1073
- id: field.id,
1074
- name,
1075
- label: field.label,
1076
- required: field.required,
1077
- value: this.formData[field.id],
1078
- help: hint,
1079
- labelClass: 'ui-dynamic-form-input-label',
1080
- inputClass: `input-${this.theme}`,
1081
- innerClass: `ui-dynamic-form-input-outlines ${
1082
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1083
- }`,
1084
- readonly: isReadOnly,
1085
- validation,
1086
- validationVisibility: 'live',
1087
- },
1088
- };
1089
- }
1090
- },
1091
- initLazyLoading() {
1092
- // Initialize Intersection Observer for lazy loading of file previews
1093
- if (typeof IntersectionObserver !== 'undefined') {
1094
- this.intersectionObserver = new IntersectionObserver(
1095
- (entries) => {
1096
- entries.forEach((entry) => {
1097
- if (entry.isIntersecting) {
1098
- const element = entry.target;
1099
- const fieldId = element.getAttribute('data-lazy-field');
1100
-
1101
- if (fieldId && !this.visibleFileFields.has(fieldId)) {
1102
- this.visibleFileFields.add(fieldId);
1103
- this.loadFilePreview(fieldId);
1104
- this.intersectionObserver.unobserve(element);
1105
- }
1106
- }
1107
- });
1108
- },
1109
- {
1110
- root: null,
1111
- rootMargin: '50px', // Start loading 50px before element becomes visible
1112
- threshold: 0.1,
1113
- }
1114
- );
1115
-
1116
- // Observe all lazy file preview elements
1117
- this.$nextTick(() => {
1118
- this.observeLazyElements();
1119
- });
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
+ }
893
+
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
+ }
902
+
903
+ if (formFields) {
904
+ formFields.forEach((field) => {
905
+ this.formData[field.id] = field.defaultValue;
906
+
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;
1120
933
  } else {
1121
- // Fallback for browsers without Intersection Observer
1122
- // Load all file previews immediately
1123
- this.loadAllFilePreviews();
934
+ this.actions = [...this.actions, ...confirmActions];
1124
935
  }
1125
- },
1126
- observeLazyElements() {
1127
- const lazyElements = document.querySelectorAll('.lazy-file-preview[data-lazy-field]');
1128
- lazyElements.forEach((element) => {
1129
- if (this.intersectionObserver) {
1130
- this.intersectionObserver.observe(element);
1131
- }
1132
- });
1133
- },
1134
- loadFilePreview(fieldId) {
1135
- // Find the field configuration
1136
- const field = this.userTask?.userTaskConfig?.formFields?.find((f) => f.id === fieldId);
1137
- if (!field) return;
1138
-
1139
- const originalFieldId = fieldId.replace('_preview', '');
1140
- const fileDataArray = this.formData[originalFieldId];
1141
-
1142
- if (!fileDataArray || fileDataArray.length === 0) return;
1143
-
1144
- const containerId = `file-preview-${fieldId}`;
1145
- const container = document.getElementById(containerId);
1146
-
1147
- if (container) {
1148
- // Show loading state
1149
- container.innerHTML = `
1150
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1151
- field.required ? ' *' : ''
1152
- }</label>
1153
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
1154
- <div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
1155
- <div>Dateien werden geladen...</div>
1156
- </div>
1157
- `;
1158
-
1159
- // Process files
1160
- setTimeout(() => {
1161
- this.processFilePreview(containerId, fileDataArray, field);
1162
- }, 0);
1163
- }
1164
- },
1165
- loadAllFilePreviews() {
1166
- // Fallback method - load all file previews immediately
1167
- const fileFields =
1168
- this.userTask?.userTaskConfig?.formFields?.filter((f) => f.type === 'file-preview') || [];
1169
- fileFields.forEach((field) => {
1170
- if (!this.visibleFileFields.has(field.id)) {
1171
- this.visibleFileFields.add(field.id);
1172
- this.loadFilePreview(field.id);
1173
- }
1174
- });
1175
- },
1176
- processFilePreview(containerId, fileDataArray, field) {
1177
- // Process files in chunks to avoid blocking the UI
1178
- const processInChunks = async () => {
1179
- const images = [];
1180
- const otherFiles = [];
1181
-
1182
- // Process files in batches to avoid UI blocking
1183
- const batchSize = 3;
936
+ }
937
+ });
938
+ }
939
+
940
+ if (initialValues) {
941
+ Object.keys(initialValues)
942
+ .filter((key) => formFieldIds.includes(key))
943
+ .forEach((key) => {
944
+ this.formData[key] = initialValues[key];
945
+ });
946
+ }
947
+
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
+ }
955
+
956
+ nextTick(() => {
957
+ this.focusFirstFormField();
958
+ });
959
+ },
960
+ actionFn(action) {
961
+ if (action.isTerminate) {
962
+ this.showError('');
963
+
964
+ const msg = this.msg ?? {};
965
+ msg.payload = {
966
+ formData: this.formData,
967
+ userTask: this.userTask,
968
+ isTerminate: true,
969
+ };
1184
970
 
1185
- for (let i = 0; i < fileDataArray.length; i += batchSize) {
1186
- const batch = fileDataArray.slice(i, i + batchSize);
971
+ const terminateOutputIndex = this.totalOutputs - 1;
1187
972
 
1188
- for (const fileData of batch) {
1189
- const fileName = fileData.name || '';
1190
- const isImage = fileName.toLowerCase().match(/\.(png|jpg|jpeg|gif|webp)$/);
973
+ this.send(msg, terminateOutputIndex);
974
+ return;
975
+ }
1191
976
 
1192
- if (isImage && fileData.file && fileData.file.data) {
1193
- // Convert buffer to base64 data URL for image display
1194
- const uint8Array = new Uint8Array(fileData.file.data);
1195
- let binaryString = '';
977
+ if (action.isSuspend) {
978
+ this.showError('');
1196
979
 
1197
- // Process in chunks to avoid call stack overflow
1198
- const chunkSize = 1024;
1199
- for (let j = 0; j < uint8Array.length; j += chunkSize) {
1200
- const chunk = uint8Array.slice(j, j + chunkSize);
1201
- binaryString += String.fromCharCode.apply(null, chunk);
1202
- }
980
+ const msg = this.msg ?? {};
981
+ msg.payload = {
982
+ formData: this.formData,
983
+ userTask: this.userTask,
984
+ isSuspend: true,
985
+ };
1203
986
 
1204
- const base64String = btoa(binaryString);
1205
- const mimeType = fileName.toLowerCase().endsWith('.png')
1206
- ? 'image/png'
1207
- : fileName.toLowerCase().endsWith('.gif')
1208
- ? 'image/gif'
1209
- : 'image/jpeg';
1210
- const dataURL = `data:${mimeType};base64,${base64String}`;
987
+ const suspendOutputIndex = this.totalOutputs - 2;
1211
988
 
1212
- images.push({ fileName, dataURL, fileData });
1213
- } else {
1214
- otherFiles.push({ fileName, fileData });
1215
- }
1216
- }
989
+ this.send(msg, suspendOutputIndex);
990
+ return;
991
+ }
1217
992
 
1218
- // Allow UI to update between batches
1219
- await new Promise((resolve) => setTimeout(resolve, 10));
1220
- }
993
+ if (action.isConfirmAction && action.confirmFieldId) {
994
+ this.formData[action.confirmFieldId] = action.confirmValue;
995
+ }
1221
996
 
1222
- // Build the final content
1223
- let content = `<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1224
- field.required ? ' *' : ''
1225
- }</label>`;
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;
1226
1000
 
1227
- // Display images
1228
- if (images.length > 0) {
1229
- content += '<div style="margin-top: 8px;">';
1230
- content += '<div style="font-weight: bold; margin-bottom: 8px;">Bilder:</div>';
1231
- images.forEach((img, index) => {
1232
- const downloadId = `download-img-${field.id}-${index}`;
1233
- content += `
1234
- <div style="display: inline-block; margin: 8px; text-align: center; vertical-align: top;">
1235
- <img src="${img.dataURL}" alt="${img.fileName}"
1236
- style="max-width: 300px; max-height: 200px; border: 1px solid #ccc; display: block; cursor: pointer;"
1237
- onclick="document.getElementById('${downloadId}').click();" />
1238
- <div style="margin-top: 4px; font-size: 0.9em; color: #666; max-width: 300px; word-break: break-word;">
1239
- ${img.fileName}
1240
- </div>
1241
- <a id="${downloadId}" href="${img.dataURL}" download="${img.fileName}" style="display: none;"></a>
1242
- </div>
1243
- `;
1244
- });
1245
- content += '</div>';
1246
- }
1001
+ formkitInputs.forEach((input) => {
1002
+ const dataComplete = input.getAttribute('data-complete');
1003
+ const dataInvalid = input.getAttribute('data-invalid');
1247
1004
 
1248
- // Display other files as list
1249
- if (otherFiles.length > 0) {
1250
- content +=
1251
- '<div style="margin-top: 12px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
1252
- content += '<div style="font-weight: bold; margin-bottom: 8px;">Weitere Dateien:</div>';
1253
- otherFiles.forEach((file, index) => {
1254
- const downloadId = `download-file-${field.id}-${index}`;
1255
- const uint8Array = new Uint8Array(file.fileData.file.data);
1256
- let binaryString = '';
1005
+ if (dataComplete == null && dataInvalid === 'true') {
1006
+ allComplete = false;
1007
+ }
1008
+ });
1257
1009
 
1258
- // Process in chunks for download
1259
- const chunkSize = 1024;
1260
- for (let j = 0; j < uint8Array.length; j += chunkSize) {
1261
- const chunk = uint8Array.slice(j, j + chunkSize);
1262
- binaryString += String.fromCharCode.apply(null, chunk);
1263
- }
1010
+ if (!allComplete) return;
1011
+ }
1264
1012
 
1265
- const base64String = btoa(binaryString);
1266
- const dataURL = `data:application/octet-stream;base64,${base64String}`;
1013
+ if (this.checkCondition(action.condition)) {
1014
+ this.showError('');
1267
1015
 
1268
- content += `
1269
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; padding: 4px; border-radius: 3px; cursor: pointer;"
1270
- onclick="document.getElementById('${downloadId}').click();"
1271
- onmouseover="this.style.backgroundColor='#e6e6e6';"
1272
- onmouseout="this.style.backgroundColor='transparent';">
1273
- <span style="font-size: 1.2em;">📎</span>
1274
- <span style="flex: 1; word-break: break-word;">${file.fileName}</span>
1275
- <span style="font-size: 0.8em; color: #007bff;">Download</span>
1276
- <a id="${downloadId}" href="${dataURL}" download="${file.fileName}" style="display: none;"></a>
1277
- </div>
1278
- `;
1279
- });
1280
- content += '</div>';
1281
- }
1016
+ const processedFormData = { ...this.formData };
1017
+ const formFields = this.userTask.userTaskConfig.formFields;
1282
1018
 
1283
- // Update the container with the final content
1284
- const container = document.getElementById(containerId);
1285
- if (container) {
1286
- container.innerHTML = content;
1287
- }
1288
- };
1019
+ formFields.forEach((field) => {
1020
+ const fieldValue = processedFormData[field.id];
1289
1021
 
1290
- processInChunks().catch((error) => {
1291
- const container = document.getElementById(containerId);
1292
- if (container) {
1293
- container.innerHTML = `
1294
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1295
- field.required ? ' *' : ''
1296
- }</label>
1297
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #d32f2f;">
1298
- <div style="font-size: 1.2em; margin-bottom: 8px;">⚠️</div>
1299
- <div>Fehler beim Laden der Dateien</div>
1300
- </div>
1301
- `;
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;
1302
1028
  }
1303
- });
1304
- },
1305
- toggleCollapse() {
1306
- this.collapsed = !this.collapsed;
1307
- },
1308
- getRowWidthStyling(field, index) {
1309
- let style = '';
1310
- if (index === 0) {
1311
- style += 'margin-top: 12px;';
1312
- }
1313
- if (field.type === 'header') {
1314
- style += 'flex-basis: 100%;';
1315
- } else {
1316
- style += `flex-basis: 100%;`;
1317
- }
1318
- return style;
1319
- },
1320
- fields() {
1321
- return this.computedFields;
1322
- },
1323
- /*
1324
- widget-action just sends a msg to Node-RED, it does not store the msg state server-side
1325
- alternatively, you can use widget-change, which will also store the msg in the Node's datastore
1326
- */
1327
- send(msg, index) {
1328
- const msgArr = [];
1329
- msgArr[index] = msg;
1330
- this.$socket.emit('widget-action', this.id, msgArr);
1331
- },
1332
- init(msg) {
1333
- this.msg = msg;
1334
- this.clearComponentCache();
1335
-
1336
- if (!msg) {
1337
- return;
1338
- }
1339
-
1340
- this.actions = this.props.options;
1341
-
1342
- const hasTask = msg.payload && msg.payload.userTask;
1343
-
1344
- if (hasTask) {
1345
- this.userTask = msg.payload.userTask;
1346
- } else {
1347
- this.userTask = null;
1348
- this.formData = {};
1349
- // Reset lazy loading state
1350
- this.visibleFileFields.clear();
1351
- return;
1352
- }
1353
-
1354
- const formFields = this.userTask.userTaskConfig.formFields;
1355
- const formFieldIds = formFields.map((ff) => ff.id);
1356
- const initialValues = this.userTask.startToken;
1357
- const finishedFormData = msg.payload.formData;
1358
- this.formIsFinished = !!msg.payload.formData;
1359
- if (this.formIsFinished) {
1360
- this.collapsed = this.props.collapse_when_finished;
1361
- }
1362
-
1363
- // Reset lazy loading state for new task
1364
- this.visibleFileFields.clear();
1365
-
1366
- if (formFields) {
1367
- formFields.forEach((field) => {
1368
- this.formData[field.id] = field.defaultValue;
1369
-
1370
- if (field.type === 'confirm') {
1371
- let customForm = {};
1372
- try {
1373
- if (field.customForm) {
1374
- if (typeof field.customForm === 'string') {
1375
- customForm = JSON.parse(field.customForm);
1376
- } else if (typeof field.customForm === 'object') {
1377
- customForm = field.customForm;
1378
- }
1379
- }
1380
- } catch (error) {
1381
- console.warn('Failed to parse customForm for confirm field', field.id, error);
1382
- customForm = {};
1383
- }
1384
- const confirmText = customForm.confirmButtonText ?? 'Confirm';
1385
- const declineText = customForm.declineButtonText ?? 'Decline';
1386
- this.actions = [
1387
- {
1388
- alignment: 'right',
1389
- primary: 'false',
1390
- label: declineText,
1391
- condition: '',
1392
- },
1393
- {
1394
- alignment: 'right',
1395
- primary: 'true',
1396
- label: confirmText,
1397
- condition: '',
1398
- },
1399
- ];
1400
- }
1401
- });
1402
-
1403
- // Check for file fields and duplicate them as file-preview if initial values exist
1404
- // Insert preview fields directly before their corresponding file fields
1405
- for (let i = formFields.length - 1; i >= 0; i--) {
1406
- const field = formFields[i];
1407
- if (field.type === 'file' && initialValues && initialValues[field.id]) {
1408
- const previewField = { ...field };
1409
- previewField.type = 'file-preview';
1410
- previewField.id = `${field.id}_preview`; // Give it a unique ID
1411
- this.userTask.userTaskConfig.formFields.splice(i, 0, previewField);
1412
- }
1029
+ } else {
1030
+ const numValue = Number.parseFloat(fieldValue);
1031
+ if (!isNaN(numValue)) {
1032
+ processedFormData[field.id] = numValue;
1413
1033
  }
1034
+ }
1414
1035
  }
1036
+ }
1037
+ });
1415
1038
 
1416
- if (initialValues) {
1417
- Object.keys(initialValues)
1418
- .filter((key) => formFieldIds.includes(key))
1419
- .forEach((key) => {
1420
- this.formData[key] = initialValues[key];
1421
- });
1422
- }
1423
-
1424
- if (this.formIsFinished) {
1425
- Object.keys(finishedFormData)
1426
- .filter((key) => formFieldIds.includes(key))
1427
- .forEach((key) => {
1428
- this.formData[key] = finishedFormData[key];
1429
- });
1430
- }
1431
-
1432
- // Force update of computed properties by triggering reactivity
1433
- this.$nextTick(() => {
1434
- this.focusFirstFormField();
1435
- // Re-observe lazy elements after DOM update
1436
- this.observeLazyElements();
1437
- });
1438
- },
1439
- actionFn(action) {
1440
- if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
1441
- const formkitInputs = this.$refs.form.$el.querySelectorAll('.formkit-outer');
1442
- let allComplete = true;
1443
-
1444
- formkitInputs.forEach((input) => {
1445
- const dataComplete = input.getAttribute('data-complete');
1446
- const dataInvalid = input.getAttribute('data-invalid');
1447
-
1448
- if (dataComplete == null && dataInvalid === 'true') {
1449
- allComplete = false;
1450
- }
1451
- });
1452
-
1453
- if (!allComplete) return;
1454
- }
1455
-
1456
- if (this.checkCondition(action.condition)) {
1457
- this.showError('');
1458
-
1459
- const processedFormData = { ...this.formData };
1460
- const formFields = this.userTask.userTaskConfig.formFields;
1461
-
1462
- formFields.forEach((field) => {
1463
- const fieldValue = processedFormData[field.id];
1464
-
1465
- if (field.type === 'number' || field.type === 'long') {
1466
- if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
1467
- if (field.type === 'long') {
1468
- const intValue = Number.parseInt(fieldValue, 10);
1469
- if (!isNaN(intValue)) {
1470
- processedFormData[field.id] = intValue;
1471
- }
1472
- } else {
1473
- const numValue = Number.parseFloat(fieldValue);
1474
- if (!isNaN(numValue)) {
1475
- processedFormData[field.id] = numValue;
1476
- }
1477
- }
1478
- }
1479
- }
1480
- });
1481
-
1482
- const msg = this.msg ?? {};
1483
- msg.payload = { formData: processedFormData, userTask: this.userTask };
1484
- this.send(
1485
- msg,
1486
- this.actions.findIndex((element) => element.label === action.label) +
1487
- (this.isConfirmDialog ? this.props.options.length : 0)
1488
- );
1489
- // TODO: mm - end
1490
- } else {
1491
- this.showError(action.errorMessage);
1492
- }
1493
- },
1494
- checkCondition(condition) {
1495
- if (condition === '') return true;
1496
- try {
1497
- // eslint-disable-next-line no-new-func
1498
- const func = Function('fields', 'userTask', 'msg', '"use strict"; return (' + condition + ')');
1499
- const result = func(this.formData, this.userTask, this.msg);
1500
- return Boolean(result);
1501
- } catch (err) {
1502
- return false;
1503
- }
1504
- },
1505
- showError(errMsg) {
1506
- this.errorMsg = errMsg;
1507
- },
1508
- focusFirstFormField() {
1509
- if (this.collapsed || !this.hasUserTask) {
1510
- return;
1511
- }
1512
-
1513
- if (this.firstFormFieldRef) {
1514
- let inputElement = null;
1515
-
1516
- if (this.firstFormFieldRef.node && this.firstFormFieldRef.node.input instanceof HTMLElement) {
1517
- inputElement = this.firstFormFieldRef.node.input;
1518
- } else if (this.firstFormFieldRef.$el instanceof HTMLElement) {
1519
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
1520
- inputElement = this.firstFormFieldRef.$el;
1521
- } else {
1522
- inputElement = this.firstFormFieldRef.$el.querySelector(
1523
- 'input:not([type="hidden"]), textarea, select'
1524
- );
1525
- }
1526
- }
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
+ }
1527
1081
 
1528
- if (inputElement) {
1529
- inputElement.focus();
1530
- }
1531
- }
1532
- },
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
+ }
1533
1088
  },
1089
+ },
1534
1090
  };
1535
1091
 
1536
1092
  function mapItems(type, field) {
1537
- if (type === 'enum') {
1538
- return field.enumValues.map((enumValue) => ({
1539
- title: enumValue.name,
1540
- value: enumValue.id,
1541
- }));
1542
- } else {
1543
- return null;
1544
- }
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
+ }
1545
1101
  }
1546
1102
  </script>
1547
1103
 
1548
1104
  <style>
1549
1105
  /* CSS is auto scoped, but using named classes is still recommended */
1550
1106
  @import '../stylesheets/ui-dynamic-form.css';
1551
-
1552
- /* Lazy loading styles */
1553
- .lazy-file-preview {
1554
- transition: opacity 0.3s ease-in-out;
1555
- }
1556
-
1557
- .lazy-file-preview .lazy-placeholder {
1558
- background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
1559
- background-size: 200% 200%;
1560
- animation: shimmer 2s infinite;
1561
- }
1562
-
1563
- @keyframes shimmer {
1564
- 0% {
1565
- background-position: -200% -200%;
1566
- }
1567
- 100% {
1568
- background-position: 200% 200%;
1569
- }
1570
- }
1571
-
1572
- .lazy-file-preview:hover {
1573
- transform: translateY(-1px);
1574
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1575
- }
1576
1107
  </style>