@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-file-preview-1bee64-mdpzqcz2 → 2.1.1

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