@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
- :style="props.title_style"
7
- :title="props.title_text"
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
- :style="props.title_style"
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 v-for="(field, index) in fields()" :key="field" :style="getRowWidthStyling(field, index)">
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
- v-model="formData[field.id]"
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
- v-model="field.defaultValue"
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-else
55
- v-bind="createComponent(field).props"
56
- v-model="formData[field.id]"
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 v-if="props.actions_inside_card && actions.length > 0" :actions="actions" :actionCallback="actionFn" :formIsFinished="formIsFinished" style="padding: 16px; padding-top: 0px;" />
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 v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0" :text="props.waiting_info" :title="props.waiting_title" />
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, UIDynamicFormFooterAction, UIDynamicFormTitleText
119
+ FormKit,
120
+ UIDynamicFormFooterAction,
121
+ UIDynamicFormTitleText
96
122
  },
97
123
  inject: ['$socket'],
98
124
  props: {
99
- /* do not remove entries from this - Dashboard's Layout Manager's will pass this data to your component */
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
- rules: {
117
- requiredIf: ({ value, name }, [targetField, expectedValue], node) => {
118
- const actual = node?.root?.value?.[targetField]
119
- if (actual === expectedValue && (!value || value === '')) {
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 this.props.options.length + (this.props.handle_confirmation_dialogs ? 2 : 0) + (this.props.trigger_on_change ? 1 : 0)
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 || this.formIsFinished || customProperties.find(entry => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'))
225
- ? 'true'
226
- : undefined
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
- widget-action just sends a msg to Node-RED, it does not store the msg state server-side
763
- alternatively, you can use widget-change, which will also store the msg in the Node's datastore
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
- alignment: 'right',
807
- primary: 'false',
808
- label: declineText,
809
- condition: ''
810
- }, {
811
- alignment: 'right',
812
- primary: 'true',
813
- label: confirmText,
814
- condition: ''
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).filter(key => formFieldIds.includes(key)).forEach((key) => {
822
- this.formData[key] = initialValues[key]
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).filter(key => formFieldIds.includes(key)).forEach(key => {
828
- this.formData[key] = finishedFormData[key]
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) + (this.isConfirmDialog ? this.props.options.length : 0)
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
  }