@5minds/node-red-dashboard-2-processcube-dynamic-form 2.0.3 → 2.0.4
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.
|
@@ -2,39 +2,37 @@
|
|
|
2
2
|
<div className="ui-dynamic-form-external-sizing-wrapper" :style="props.card_size_styling">
|
|
3
3
|
<!-- Component must be wrapped in a block so props such as className and style can be passed in from parent -->
|
|
4
4
|
<UIDynamicFormTitleText
|
|
5
|
-
v-if="props.title_style === 'outside' && hasUserTask"
|
|
6
|
-
:
|
|
7
|
-
:
|
|
8
|
-
:customStyles="props.title_custom_text_styling"
|
|
9
|
-
:titleIcon="props.title_icon"
|
|
10
|
-
:collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
|
|
11
|
-
:collapsed="collapsed"
|
|
5
|
+
v-if="props.title_style === 'outside' && hasUserTask" :style="props.title_style"
|
|
6
|
+
:title="props.title_text" :customStyles="props.title_custom_text_styling" :titleIcon="props.title_icon"
|
|
7
|
+
:collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)" :collapsed="collapsed"
|
|
12
8
|
:toggleCollapse="toggleCollapse"
|
|
13
9
|
/>
|
|
14
10
|
<div className="ui-dynamic-form-wrapper">
|
|
15
|
-
<p v-if="hasUserTask" style="margin-bottom: 0px
|
|
11
|
+
<p v-if="hasUserTask" style="margin-bottom: 0px">
|
|
16
12
|
<v-form ref="form" v-model="form" :class="dynamicClass">
|
|
17
13
|
<UIDynamicFormTitleText
|
|
18
|
-
v-if="props.title_style != 'outside'"
|
|
19
|
-
:
|
|
20
|
-
:title="props.title_text"
|
|
21
|
-
:customStyles="props.title_custom_text_styling"
|
|
14
|
+
v-if="props.title_style != 'outside'" :style="props.title_style"
|
|
15
|
+
:title="props.title_text" :customStyles="props.title_custom_text_styling"
|
|
22
16
|
:titleIcon="props.title_icon"
|
|
23
17
|
:collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
|
|
24
|
-
:collapsed="collapsed"
|
|
25
|
-
:toggleCollapse="toggleCollapse"
|
|
18
|
+
:collapsed="collapsed" :toggleCollapse="toggleCollapse"
|
|
26
19
|
/>
|
|
27
20
|
<Transition name="cardCollapse">
|
|
28
21
|
<div v-if="!collapsed">
|
|
29
|
-
<div className="ui-dynamic-form-formfield-positioner">
|
|
22
|
+
<div className="ui-dynamic-form-formfield-positioner" :style="props.inner_card_styling">
|
|
30
23
|
<FormKit id="form" type="group">
|
|
31
|
-
<v-row
|
|
24
|
+
<v-row
|
|
25
|
+
v-for="(field, index) in fields()" :key="field"
|
|
26
|
+
:style="getRowWidthStyling(field, index)"
|
|
27
|
+
>
|
|
32
28
|
<v-col cols="12">
|
|
33
29
|
<component
|
|
34
30
|
:is="createComponent(field).type"
|
|
35
31
|
v-if="createComponent(field).innerText"
|
|
36
|
-
v-bind="createComponent(field).props"
|
|
37
|
-
|
|
32
|
+
v-bind="createComponent(field).props" :ref="(el) => {
|
|
33
|
+
if (index === 0) firstFormFieldRef = el;
|
|
34
|
+
}
|
|
35
|
+
" v-model="formData[field.id]"
|
|
38
36
|
>
|
|
39
37
|
{{ createComponent(field).innerText }}
|
|
40
38
|
</component>
|
|
@@ -42,18 +40,22 @@
|
|
|
42
40
|
<p class="formkit-label">{{ field.label }}</p>
|
|
43
41
|
<component
|
|
44
42
|
:is="createComponent(field).type"
|
|
45
|
-
v-bind="createComponent(field).props"
|
|
46
|
-
|
|
43
|
+
v-bind="createComponent(field).props" :ref="(el) => {
|
|
44
|
+
if (index === 0) firstFormFieldRef = el;
|
|
45
|
+
}
|
|
46
|
+
" v-model="field.defaultValue"
|
|
47
47
|
/>
|
|
48
48
|
<p class="formkit-help">
|
|
49
|
-
{{ field.customForm ? JSON.parse(field.customForm).hint : undefined
|
|
49
|
+
{{ field.customForm ? JSON.parse(field.customForm).hint : undefined
|
|
50
|
+
}}
|
|
50
51
|
</p>
|
|
51
52
|
</div>
|
|
52
53
|
<component
|
|
53
|
-
:is="createComponent(field).type"
|
|
54
|
-
v-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
:is="createComponent(field).type" v-else
|
|
55
|
+
v-bind="createComponent(field).props" :ref="(el) => {
|
|
56
|
+
if (index === 0) firstFormFieldRef = el;
|
|
57
|
+
}
|
|
58
|
+
" v-model="formData[field.id]"
|
|
57
59
|
/>
|
|
58
60
|
</v-col>
|
|
59
61
|
</v-row>
|
|
@@ -63,17 +65,24 @@
|
|
|
63
65
|
<v-row v-if="errorMsg.length > 0" style="padding: 12px">
|
|
64
66
|
<v-alert type="error">Error: {{ errorMsg }}</v-alert>
|
|
65
67
|
</v-row>
|
|
66
|
-
<UIDynamicFormFooterAction
|
|
68
|
+
<UIDynamicFormFooterAction
|
|
69
|
+
v-if="props.actions_inside_card && actions.length > 0"
|
|
70
|
+
:actions="actions" :actionCallback="actionFn" :formIsFinished="formIsFinished"
|
|
71
|
+
style="padding: 16px; padding-top: 0px"
|
|
72
|
+
/>
|
|
67
73
|
</v-row>
|
|
68
74
|
</div>
|
|
69
75
|
</Transition>
|
|
70
76
|
</v-form>
|
|
71
77
|
</p>
|
|
72
78
|
<p v-else>
|
|
73
|
-
<v-alert
|
|
79
|
+
<v-alert
|
|
80
|
+
v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
|
|
81
|
+
:text="props.waiting_info" :title="props.waiting_title"
|
|
82
|
+
/>
|
|
74
83
|
</p>
|
|
75
84
|
</div>
|
|
76
|
-
<div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px
|
|
85
|
+
<div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px">
|
|
77
86
|
<UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
|
|
78
87
|
</div>
|
|
79
88
|
</div>
|
|
@@ -81,22 +90,39 @@
|
|
|
81
90
|
|
|
82
91
|
<!-- eslint-disable no-case-declarations -->
|
|
83
92
|
<script>
|
|
93
|
+
import { de } from '@formkit/i18n'
|
|
84
94
|
import { FormKit, defaultConfig, plugin } from '@formkit/vue'
|
|
85
|
-
import { getCurrentInstance, markRaw } from 'vue'
|
|
95
|
+
import { getCurrentInstance, markRaw, nextTick } from 'vue'
|
|
86
96
|
|
|
87
97
|
// eslint-disable-next-line import/no-unresolved
|
|
88
98
|
import '@formkit/themes/genesis'
|
|
89
99
|
import UIDynamicFormFooterAction from './FooterActions.vue'
|
|
90
100
|
import UIDynamicFormTitleText from './TitleText.vue'
|
|
91
101
|
|
|
102
|
+
// eslint-disable-next-line no-unused-vars
|
|
103
|
+
function requiredIf ({ value }, [targetField, expectedValue], node) {
|
|
104
|
+
console.debug(arguments)
|
|
105
|
+
|
|
106
|
+
const actual = node?.root?.value?.[targetField]
|
|
107
|
+
const isEmpty = value === '' || value === null || value === undefined
|
|
108
|
+
|
|
109
|
+
if (actual === expectedValue && isEmpty) {
|
|
110
|
+
return false // oder: `return "Dieses Feld ist erforderlich."`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
export default {
|
|
93
117
|
name: 'UIDynamicForm',
|
|
94
118
|
components: {
|
|
95
|
-
FormKit,
|
|
119
|
+
FormKit,
|
|
120
|
+
UIDynamicFormFooterAction,
|
|
121
|
+
UIDynamicFormTitleText
|
|
96
122
|
},
|
|
97
123
|
inject: ['$socket'],
|
|
98
124
|
props: {
|
|
99
|
-
|
|
125
|
+
/* do not remove entries from this - Dashboard's Layout Manager's will pass this data to your component */
|
|
100
126
|
id: { type: String, required: true },
|
|
101
127
|
props: { type: Object, default: () => ({}) },
|
|
102
128
|
state: {
|
|
@@ -113,20 +139,10 @@ export default {
|
|
|
113
139
|
|
|
114
140
|
const formkitConfig = defaultConfig({
|
|
115
141
|
theme: 'genesis',
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return `Feld ${name} ist erforderlich, wenn ${targetField} = ${expectedValue}`
|
|
121
|
-
}
|
|
122
|
-
return true
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
messages: {
|
|
126
|
-
de: {
|
|
127
|
-
requiredIf: 'Dieses Feld ist erforderlich.'
|
|
128
|
-
}
|
|
129
|
-
}
|
|
142
|
+
locales: { de },
|
|
143
|
+
locale: 'de',
|
|
144
|
+
// eslint-disable-next-line object-shorthand
|
|
145
|
+
rules: { requiredIf: requiredIf }
|
|
130
146
|
})
|
|
131
147
|
app.use(plugin, formkitConfig)
|
|
132
148
|
},
|
|
@@ -139,7 +155,8 @@ export default {
|
|
|
139
155
|
errorMsg: '',
|
|
140
156
|
formIsFinished: false,
|
|
141
157
|
msg: null,
|
|
142
|
-
collapsed: false
|
|
158
|
+
collapsed: false,
|
|
159
|
+
firstFormFieldRef: null
|
|
143
160
|
}
|
|
144
161
|
},
|
|
145
162
|
computed: {
|
|
@@ -153,10 +170,14 @@ export default {
|
|
|
153
170
|
return !!this.userTask
|
|
154
171
|
},
|
|
155
172
|
totalOutputs () {
|
|
156
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
this.props.options.length +
|
|
175
|
+
(this.props.handle_confirmation_dialogs ? 2 : 0) +
|
|
176
|
+
(this.props.trigger_on_change ? 1 : 0)
|
|
177
|
+
)
|
|
157
178
|
},
|
|
158
179
|
isConfirmDialog () {
|
|
159
|
-
return this.userTask.userTaskConfig.formFields.some(field => field.type === 'confirm')
|
|
180
|
+
return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm')
|
|
160
181
|
}
|
|
161
182
|
},
|
|
162
183
|
watch: {
|
|
@@ -167,6 +188,20 @@ export default {
|
|
|
167
188
|
this.send(res, this.totalOutputs - 1)
|
|
168
189
|
}
|
|
169
190
|
},
|
|
191
|
+
collapsed (newVal) {
|
|
192
|
+
if (!newVal && this.hasUserTask) {
|
|
193
|
+
nextTick(() => {
|
|
194
|
+
this.focusFirstFormField()
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
userTask (newVal) {
|
|
199
|
+
if (newVal && !this.collapsed) {
|
|
200
|
+
nextTick(() => {
|
|
201
|
+
this.focusFirstFormField()
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
},
|
|
170
205
|
deep: true
|
|
171
206
|
}
|
|
172
207
|
},
|
|
@@ -220,10 +255,12 @@ export default {
|
|
|
220
255
|
const validation = customForm.validation
|
|
221
256
|
const name = field.id
|
|
222
257
|
const customProperties = customForm.customProperties ?? []
|
|
223
|
-
const isReadOnly =
|
|
224
|
-
this.props.readonly ||
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
const isReadOnly =
|
|
259
|
+
this.props.readonly ||
|
|
260
|
+
this.formIsFinished ||
|
|
261
|
+
customProperties.find((entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true')
|
|
262
|
+
? 'true'
|
|
263
|
+
: undefined
|
|
227
264
|
switch (field.type) {
|
|
228
265
|
case 'long':
|
|
229
266
|
return {
|
|
@@ -386,6 +423,7 @@ export default {
|
|
|
386
423
|
}
|
|
387
424
|
}
|
|
388
425
|
case 'file':
|
|
426
|
+
const multiple = field.customForm ? JSON.parse(field.customForm).multiple === 'true' : false
|
|
389
427
|
return {
|
|
390
428
|
type: 'FormKit',
|
|
391
429
|
props: {
|
|
@@ -403,6 +441,7 @@ export default {
|
|
|
403
441
|
// innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
|
|
404
442
|
readonly: isReadOnly,
|
|
405
443
|
disabled: isReadOnly,
|
|
444
|
+
multiple,
|
|
406
445
|
validation,
|
|
407
446
|
validationVisibility: 'live'
|
|
408
447
|
}
|
|
@@ -745,7 +784,7 @@ export default {
|
|
|
745
784
|
if (field.type === 'header') {
|
|
746
785
|
style += 'flex-basis: 100%;'
|
|
747
786
|
} else {
|
|
748
|
-
style += `flex-basis: ${1 / this.props.form_columns * 100}%;`
|
|
787
|
+
style += `flex-basis: ${(1 / this.props.form_columns) * 100}%;`
|
|
749
788
|
}
|
|
750
789
|
return style
|
|
751
790
|
},
|
|
@@ -759,9 +798,9 @@ export default {
|
|
|
759
798
|
return fieldMap
|
|
760
799
|
},
|
|
761
800
|
/*
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
801
|
+
widget-action just sends a msg to Node-RED, it does not store the msg state server-side
|
|
802
|
+
alternatively, you can use widget-change, which will also store the msg in the Node's datastore
|
|
803
|
+
*/
|
|
765
804
|
send (msg, index) {
|
|
766
805
|
const msgArr = []
|
|
767
806
|
msgArr[index] = msg
|
|
@@ -786,7 +825,7 @@ export default {
|
|
|
786
825
|
}
|
|
787
826
|
|
|
788
827
|
const formFields = this.userTask.userTaskConfig.formFields
|
|
789
|
-
const formFieldIds = formFields.map(ff => ff.id)
|
|
828
|
+
const formFieldIds = formFields.map((ff) => ff.id)
|
|
790
829
|
const initialValues = this.userTask.startToken
|
|
791
830
|
const finishedFormData = msg.payload.formData
|
|
792
831
|
this.formIsFinished = !!msg.payload.formData
|
|
@@ -802,32 +841,43 @@ export default {
|
|
|
802
841
|
const customForm = field.customForm ? JSON.parse(field.customForm) : {}
|
|
803
842
|
const confirmText = customForm.confirmButtonText ?? 'Confirm'
|
|
804
843
|
const declineText = customForm.declineButtonText ?? 'Decline'
|
|
805
|
-
this.actions = [
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
844
|
+
this.actions = [
|
|
845
|
+
{
|
|
846
|
+
alignment: 'right',
|
|
847
|
+
primary: 'false',
|
|
848
|
+
label: declineText,
|
|
849
|
+
condition: ''
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
alignment: 'right',
|
|
853
|
+
primary: 'true',
|
|
854
|
+
label: confirmText,
|
|
855
|
+
condition: ''
|
|
856
|
+
}
|
|
857
|
+
]
|
|
816
858
|
}
|
|
817
859
|
})
|
|
818
860
|
}
|
|
819
861
|
|
|
820
862
|
if (initialValues) {
|
|
821
|
-
Object.keys(initialValues)
|
|
822
|
-
|
|
823
|
-
|
|
863
|
+
Object.keys(initialValues)
|
|
864
|
+
.filter((key) => formFieldIds.includes(key))
|
|
865
|
+
.forEach((key) => {
|
|
866
|
+
this.formData[key] = initialValues[key]
|
|
867
|
+
})
|
|
824
868
|
}
|
|
825
869
|
|
|
826
870
|
if (this.formIsFinished) {
|
|
827
|
-
Object.keys(finishedFormData)
|
|
828
|
-
|
|
829
|
-
|
|
871
|
+
Object.keys(finishedFormData)
|
|
872
|
+
.filter((key) => formFieldIds.includes(key))
|
|
873
|
+
.forEach((key) => {
|
|
874
|
+
this.formData[key] = finishedFormData[key]
|
|
875
|
+
})
|
|
830
876
|
}
|
|
877
|
+
|
|
878
|
+
nextTick(() => {
|
|
879
|
+
this.focusFirstFormField()
|
|
880
|
+
})
|
|
831
881
|
},
|
|
832
882
|
actionFn (action) {
|
|
833
883
|
if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
|
|
@@ -857,7 +907,8 @@ export default {
|
|
|
857
907
|
msg.payload = { formData: this.formData, userTask: this.userTask }
|
|
858
908
|
this.send(
|
|
859
909
|
msg,
|
|
860
|
-
this.actions.findIndex((element) => element.label === action.label) +
|
|
910
|
+
this.actions.findIndex((element) => element.label === action.label) +
|
|
911
|
+
(this.isConfirmDialog ? this.props.options.length : 0)
|
|
861
912
|
)
|
|
862
913
|
// TODO: mm - end
|
|
863
914
|
} else {
|
|
@@ -878,6 +929,31 @@ export default {
|
|
|
878
929
|
},
|
|
879
930
|
showError (errMsg) {
|
|
880
931
|
this.errorMsg = errMsg
|
|
932
|
+
},
|
|
933
|
+
focusFirstFormField () {
|
|
934
|
+
if (this.collapsed || !this.hasUserTask) {
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (this.firstFormFieldRef) {
|
|
939
|
+
let inputElement = null
|
|
940
|
+
|
|
941
|
+
if (this.firstFormFieldRef.node && this.firstFormFieldRef.node.input instanceof HTMLElement) {
|
|
942
|
+
inputElement = this.firstFormFieldRef.node.input
|
|
943
|
+
} else if (this.firstFormFieldRef.$el instanceof HTMLElement) {
|
|
944
|
+
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
|
|
945
|
+
inputElement = this.firstFormFieldRef.$el
|
|
946
|
+
} else {
|
|
947
|
+
inputElement = this.firstFormFieldRef.$el.querySelector('input:not([type="hidden"]), textarea, select')
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (inputElement) {
|
|
952
|
+
inputElement.focus()
|
|
953
|
+
} else {
|
|
954
|
+
console.warn('Could not find a focusable input element for the first form field.')
|
|
955
|
+
}
|
|
956
|
+
}
|
|
881
957
|
}
|
|
882
958
|
}
|
|
883
959
|
}
|