@5minds/node-red-dashboard-2-processcube-dynamic-form 2.0.3-feature-80b521-mbt3b80e → 2.0.3-feature-6a41d1-mcdc8slf

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: {
@@ -107,10 +133,16 @@ export default {
107
133
  setup (props) {
108
134
  console.info('UIDynamicForm setup with:', props)
109
135
  console.debug('Vue function loaded correctly', markRaw)
136
+
110
137
  const instance = getCurrentInstance()
111
138
  const app = instance.appContext.app
139
+
112
140
  const formkitConfig = defaultConfig({
113
- theme: 'genesis'
141
+ theme: 'genesis',
142
+ locales: { de },
143
+ locale: 'de',
144
+ // eslint-disable-next-line object-shorthand
145
+ rules: { requiredIf: requiredIf }
114
146
  })
115
147
  app.use(plugin, formkitConfig)
116
148
  },
@@ -123,7 +155,8 @@ export default {
123
155
  errorMsg: '',
124
156
  formIsFinished: false,
125
157
  msg: null,
126
- collapsed: false
158
+ collapsed: false,
159
+ firstFormFieldRef: null
127
160
  }
128
161
  },
129
162
  computed: {
@@ -137,10 +170,14 @@ export default {
137
170
  return !!this.userTask
138
171
  },
139
172
  totalOutputs () {
140
- 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
+ )
141
178
  },
142
179
  isConfirmDialog () {
143
- return this.userTask.userTaskConfig.formFields.some(field => field.type === 'confirm')
180
+ return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm')
144
181
  }
145
182
  },
146
183
  watch: {
@@ -151,6 +188,20 @@ export default {
151
188
  this.send(res, this.totalOutputs - 1)
152
189
  }
153
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
+ },
154
205
  deep: true
155
206
  }
156
207
  },
@@ -204,10 +255,12 @@ export default {
204
255
  const validation = customForm.validation
205
256
  const name = field.id
206
257
  const customProperties = customForm.customProperties ?? []
207
- const isReadOnly = (
208
- this.props.readonly || this.formIsFinished || customProperties.find(entry => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'))
209
- ? 'true'
210
- : 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
211
264
  switch (field.type) {
212
265
  case 'long':
213
266
  return {
@@ -731,7 +784,7 @@ export default {
731
784
  if (field.type === 'header') {
732
785
  style += 'flex-basis: 100%;'
733
786
  } else {
734
- style += `flex-basis: ${1 / this.props.form_columns * 100}%;`
787
+ style += `flex-basis: ${(1 / this.props.form_columns) * 100}%;`
735
788
  }
736
789
  return style
737
790
  },
@@ -745,9 +798,9 @@ export default {
745
798
  return fieldMap
746
799
  },
747
800
  /*
748
- widget-action just sends a msg to Node-RED, it does not store the msg state server-side
749
- alternatively, you can use widget-change, which will also store the msg in the Node's datastore
750
- */
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
+ */
751
804
  send (msg, index) {
752
805
  const msgArr = []
753
806
  msgArr[index] = msg
@@ -772,7 +825,7 @@ export default {
772
825
  }
773
826
 
774
827
  const formFields = this.userTask.userTaskConfig.formFields
775
- const formFieldIds = formFields.map(ff => ff.id)
828
+ const formFieldIds = formFields.map((ff) => ff.id)
776
829
  const initialValues = this.userTask.startToken
777
830
  const finishedFormData = msg.payload.formData
778
831
  this.formIsFinished = !!msg.payload.formData
@@ -788,32 +841,43 @@ export default {
788
841
  const customForm = field.customForm ? JSON.parse(field.customForm) : {}
789
842
  const confirmText = customForm.confirmButtonText ?? 'Confirm'
790
843
  const declineText = customForm.declineButtonText ?? 'Decline'
791
- this.actions = [{
792
- alignment: 'right',
793
- primary: 'false',
794
- label: declineText,
795
- condition: ''
796
- }, {
797
- alignment: 'right',
798
- primary: 'true',
799
- label: confirmText,
800
- condition: ''
801
- }]
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
+ ]
802
858
  }
803
859
  })
804
860
  }
805
861
 
806
862
  if (initialValues) {
807
- Object.keys(initialValues).filter(key => formFieldIds.includes(key)).forEach((key) => {
808
- this.formData[key] = initialValues[key]
809
- })
863
+ Object.keys(initialValues)
864
+ .filter((key) => formFieldIds.includes(key))
865
+ .forEach((key) => {
866
+ this.formData[key] = initialValues[key]
867
+ })
810
868
  }
811
869
 
812
870
  if (this.formIsFinished) {
813
- Object.keys(finishedFormData).filter(key => formFieldIds.includes(key)).forEach(key => {
814
- this.formData[key] = finishedFormData[key]
815
- })
871
+ Object.keys(finishedFormData)
872
+ .filter((key) => formFieldIds.includes(key))
873
+ .forEach((key) => {
874
+ this.formData[key] = finishedFormData[key]
875
+ })
816
876
  }
877
+
878
+ nextTick(() => {
879
+ this.focusFirstFormField()
880
+ })
817
881
  },
818
882
  actionFn (action) {
819
883
  if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
@@ -843,7 +907,8 @@ export default {
843
907
  msg.payload = { formData: this.formData, userTask: this.userTask }
844
908
  this.send(
845
909
  msg,
846
- 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)
847
912
  )
848
913
  // TODO: mm - end
849
914
  } else {
@@ -864,6 +929,31 @@ export default {
864
929
  },
865
930
  showError (errMsg) {
866
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
+ }
867
957
  }
868
958
  }
869
959
  }