@live-change/frontend-auto-form 0.9.77 → 0.9.79

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.
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <div class="flex flex-col-reverse md:flex-row justify-between items-center">
3
+ <div class="flex flex-col mt-2 md:mt-0">
4
+ <div v-if="savingDraft" class="text-surface-500 dark:text-surface-300 mr-2 flex flex-row items-center">
5
+ <i class="pi pi-spin pi-spinner mr-2" style="font-size: 1.23rem"></i>
6
+ <span>Executing...</span>
7
+ </div>
8
+ <div v-else-if="draftChanged" class="text-sm text-surface-500 dark:text-surface-300 mr-2">
9
+ Draft changed
10
+ </div>
11
+ <Message v-else-if="validationResult" severity="error" variant="simple" size="small" class="mr-2">
12
+ Before running, please correct the errors above.
13
+ </Message>
14
+ </div>
15
+ <div class="flex flex-row">
16
+ <slot name="submit" v-if="!validationResult">
17
+ <div class="ml-2">
18
+ <Button
19
+ type="submit"
20
+ :label="submitting === true ? 'Executing...' : 'Execute'"
21
+ :icon="submitting === true ? 'pi pi-spin pi-spinner' : 'pi pi-play'"
22
+ :disabled="submitting"
23
+ />
24
+ </div>
25
+ </slot>
26
+ <slot name="reset" v-if="resetButton">
27
+ <div>
28
+ <Button type="reset" label="Reset" class="ml-2" :disabled="!changed" icon="pi pi-eraser"/>
29
+ </div>
30
+ </slot>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup>
36
+
37
+ import Message from "primevue/message"
38
+
39
+ import { ref, computed, onMounted, defineProps, defineEmits, toRefs, getCurrentInstance, unref } from 'vue'
40
+
41
+ const props = defineProps({
42
+ actionFormData: {
43
+ type: Object,
44
+ required: true,
45
+ },
46
+ resetButton: {
47
+ type: Boolean,
48
+ required: true,
49
+ },
50
+ options: {
51
+ type: Object,
52
+ default: () => ({})
53
+ },
54
+ i18n: {
55
+ type: String,
56
+ default: ''
57
+ }
58
+ })
59
+ const { actionFormData, resetButton, options, i18n } = toRefs(props)
60
+
61
+ const changed = computed(() => unref(actionFormData).changed.value)
62
+ const draftChanged = computed(() => unref(actionFormData).draftChanged?.value)
63
+ const savingDraft = computed(() => unref(actionFormData).savingDraft?.value)
64
+ const submitting = computed(() => unref(actionFormData).submitting?.value)
65
+ const propertiesErrors = computed(() => unref(actionFormData).propertiesErrors?.value)
66
+
67
+
68
+ const validationResult = computed(() => {
69
+ const errors = propertiesErrors.value
70
+ if(errors && Object.keys(errors).length > 0) {
71
+ return errors
72
+ }
73
+ return null
74
+ })
75
+
76
+ </script>
77
+
78
+ <style scoped>
79
+
80
+ </style>
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <div v-if="actionFormData">
3
+ <div class="text-xl mb-2">
4
+ Service <strong>{{ service }}</strong>
5
+ </div>
6
+ <div class="text-2xl mb-6">
7
+ Action <strong>{{ action }}</strong>
8
+ </div>
9
+
10
+ <form @submit="handleSubmit" @reset="handleReset">
11
+ <div class="flex flex-col gap-4">
12
+ <auto-editor
13
+ :definition="actionFormData.action.definition"
14
+ v-model="actionFormData.value"
15
+ :rootValue="actionFormData.value"
16
+ :errors="actionFormData.propertiesErrors"
17
+ :i18n="i18n"
18
+ />
19
+
20
+ <ActionButtons
21
+ :actionFormData="actionFormData"
22
+ :resetButton="true"
23
+ :i18n="i18n"
24
+ />
25
+
26
+ <!-- <div class="flex justify-end mt-4 gap-2">
27
+ <Button
28
+ type="reset"
29
+ :label="'Reset'"
30
+ :icon="'pi pi-times'"
31
+ :disabled="actionFormData.submitting"
32
+ severity="danger"
33
+ />
34
+ <Button
35
+ type="submit"
36
+ :label="actionFormData.submitting === true ? 'Executing...' : 'Execute'"
37
+ :icon="actionFormData.submitting === true ? 'pi pi-spin pi-spinner' : 'pi pi-play'"
38
+ :disabled="actionFormData.submitting"
39
+ severity="success"
40
+ />
41
+ </div> -->
42
+ </div>
43
+ </form>
44
+
45
+ <!-- <pre>propertiesErrors = {{ actionFormData.propertiesErrors }}</pre>
46
+ <pre>definition = {{ actionFormData.action.definition }}</pre> -->
47
+ </div>
48
+ </template>
49
+
50
+ <script setup>
51
+
52
+ import { ref, computed, onMounted, defineProps, defineEmits, toRefs } from 'vue'
53
+ import Button from 'primevue/button'
54
+ import AutoEditor from '../form/AutoEditor.vue'
55
+ import ActionButtons from './ActionButtons.vue'
56
+ import { useToast } from 'primevue/usetoast'
57
+ const toast = useToast()
58
+
59
+ const props = defineProps({
60
+ service: {
61
+ type: String,
62
+ required: true,
63
+ },
64
+ action: {
65
+ type: String,
66
+ required: true,
67
+ },
68
+ i18n: {
69
+ type: String,
70
+ default: ''
71
+ }
72
+ })
73
+
74
+ const { service, action, i18n } = toRefs(props)
75
+
76
+ const emit = defineEmits(['done', 'error'])
77
+
78
+ import { useApi } from '@live-change/vue3-ssr'
79
+ const api = useApi()
80
+
81
+ import { actionData } from '@live-change/frontend-auto-form'
82
+
83
+ const actionFormData = await actionData({
84
+ service: service.value,
85
+ action: action.value,
86
+ i18n: i18n.value
87
+ })
88
+
89
+ const handleSubmit = (ev) => {
90
+ ev.preventDefault()
91
+ actionFormData.submit()
92
+ }
93
+
94
+ const handleReset = (ev) => {
95
+ ev.preventDefault()
96
+ actionFormData.reset()
97
+ }
98
+
99
+ </script>
100
+
101
+ <style scoped>
102
+ </style>
@@ -66,22 +66,17 @@
66
66
  const savingDraft = computed(() => editor.value.savingDraft?.value)
67
67
  const sourceChanged = computed(() => editor.value.sourceChanged?.value)
68
68
  const saving = computed(() => editor.value.saving?.value)
69
-
69
+ const propertiesErrors = computed(() => editor.value.propertiesErrors?.value)
70
+
70
71
  const appContext = getCurrentInstance().appContext
71
72
 
72
73
  import { validateData } from "@live-change/vue3-components"
73
74
  const validationResult = computed(() => {
74
- const currentValue = {
75
- ...(editor.value.identifiers),
76
- ...unref(editor.value.value),
75
+ const errors = propertiesErrors.value
76
+ if(errors && Object.keys(errors).length > 0) {
77
+ return errors
77
78
  }
78
- const validationResult = validateData(model.value, currentValue, 'validation', appContext,
79
- props.propName, props.rootValue, true)
80
- const softValidationResult = validateData(model.value, currentValue, 'softValidation', appContext,
81
- props.propName, props.rootValue, true)
82
- console.log("currentValue", currentValue)
83
- console.log("validationResult", validationResult, softValidationResult)
84
- return validationResult || softValidationResult
79
+ return null
85
80
  })
86
81
 
87
82
  </script>
@@ -60,6 +60,7 @@
60
60
  :definition="modelDefinition"
61
61
  v-model="editor.value.value"
62
62
  :rootValue="editor.value.value"
63
+ :errors="editor.propertiesErrors"
63
64
  :i18n="i18n" />
64
65
  <EditorButtons :editor="editor" reset-button />
65
66
  </form>
@@ -47,7 +47,8 @@
47
47
  </div>
48
48
  <auto-input :modelValue="value" :definition="definition.of || definition.items"
49
49
  @update:modelValue="value => updateItem(index, value)"
50
- :rootValue="props.rootValue" :propName="props.propName + '.' + index"
50
+ :rootValue="props.rootValue" :errors="props.errors"
51
+ :propName="props.propName + '.' + index"
51
52
  :i18n="i18n" />
52
53
  </div>
53
54
  <div>
@@ -82,6 +83,10 @@
82
83
  type: Object,
83
84
  default: () => ({})
84
85
  },
86
+ errors: {
87
+ type: Object,
88
+ default: () => ({})
89
+ },
85
90
  propName: {
86
91
  type: String,
87
92
  default: ''
@@ -5,8 +5,9 @@
5
5
  @update:modelValue="value => updateModelProperty(property, value)"
6
6
  :definition="definition.properties[property]"
7
7
  :label="property"
8
- :rootValue="props.rootValue" :propName="(propName ? propName + '.' : '') + property"
9
- :i18n="i18n"
8
+ :rootValue="props.rootValue" :errors="props.errors"
9
+ :propName="(propName ? propName + '.' : '') + property"
10
+ :i18n="i18n"
10
11
  class="col-span-12" />
11
12
  </div>
12
13
  </template>
@@ -38,10 +39,14 @@
38
39
  items: {
39
40
  type: String
40
41
  }
42
+ },
43
+ errors: {
44
+ type: Object,
45
+ default: () => ({})
41
46
  }
42
47
  })
43
48
 
44
- const { modelValue, definition, propName, editableProperties } = toRefs(props)
49
+ const { modelValue, definition, propName, editableProperties, errors } = toRefs(props)
45
50
 
46
51
  const emit = defineEmits(['update:modelValue'])
47
52
 
@@ -58,7 +63,6 @@
58
63
  emit('update:modelValue', data)
59
64
  }
60
65
 
61
-
62
66
  </script>
63
67
 
64
68
  <style scoped>
@@ -10,7 +10,7 @@
10
10
  :class="props.inputClass" :style="props.inputStyle"
11
11
  :attributes="props.inputAttributes"
12
12
  :propName="props.propName"
13
- :rootValue="props.rootValue"
13
+ :rootValue="props.rootValue" :errors="props.errors"
14
14
  @update:modelValue="value => emit('update:modelValue', value)"
15
15
  :id="uid"
16
16
  :i18n="i18n" />
@@ -81,6 +81,10 @@
81
81
  type: Object,
82
82
  default: () => ({})
83
83
  },
84
+ errors: {
85
+ type: Object,
86
+ default: () => ({})
87
+ },
84
88
  propName: {
85
89
  type: String,
86
90
  default: ''
@@ -99,7 +103,7 @@
99
103
 
100
104
  const emit = defineEmits(['update:modelValue'])
101
105
 
102
- const { error, definition, modelValue } = toRefs(props)
106
+ const { error, definition, modelValue, errors } = toRefs(props)
103
107
 
104
108
  const definitionIf = computed(() => {
105
109
  if(definition.value?.if) {
@@ -129,12 +133,8 @@
129
133
 
130
134
  import { validateData } from "@live-change/vue3-components"
131
135
  const appContext = getCurrentInstance().appContext
132
- const validationResult = computed(() => {
133
- const validationResult = validateData(definition.value, modelValue.value, 'validation', appContext,
134
- props.propName, props.rootValue, true)
135
- const softValidationResult = validateData(definition.value, modelValue.value, 'softValidation', appContext,
136
- props.propName, props.rootValue, true)
137
- return validationResult || softValidationResult || error.value
136
+ const validationResult = computed(() => {
137
+ return errors.value?.[props.propName] || error.value
138
138
  })
139
139
 
140
140
  function findValidation(name) {
@@ -189,6 +189,7 @@
189
189
  inputClass: [props.inputClass, { 'p-invalid': !!validationResult.value }],
190
190
  inputStyle: props.inputStyle,
191
191
  rootValue: props.rootValue,
192
+ errors: props.errors,
192
193
  propName: props.propName,
193
194
  }))
194
195
 
@@ -27,6 +27,10 @@
27
27
  type: Object,
28
28
  default: () => ({})
29
29
  },
30
+ errors: {
31
+ type: Object,
32
+ default: () => ({})
33
+ },
30
34
  propName: {
31
35
  type: String,
32
36
  default: ''
@@ -85,6 +89,7 @@
85
89
  propName: props.propName,
86
90
  fieldName,
87
91
  rootValue: props.rootValue,
92
+ errors: props.errors,
88
93
  t, d, n, rt, te
89
94
  })
90
95
  }
@@ -98,6 +103,7 @@
98
103
  modelValue: modelValue.value,
99
104
  definition: definition.value,
100
105
  rootValue: props.rootValue,
106
+ errors: props.errors,
101
107
  propName: props.propName,
102
108
  i18n: props.i18n
103
109
  }))
@@ -4,7 +4,8 @@
4
4
  <auto-input :modelValue="modelValue" :definition="definition" :name="props.name"
5
5
  :class="props.inputClass" :style="props.inputStyle"
6
6
  :properties="props.inputAttributes"
7
- :rootValue="props.rootValue" :propName="props.propName"
7
+ :rootValue="props.rootValue" :errors="props.errors"
8
+ :propName="props.propName"
8
9
  @update:modelValue="value => emit('update:modelValue', value)"
9
10
  :i18n="props.i18n + props.propName.split('.').pop() + '.'" />
10
11
  <Message v-if="typeof validationResult == 'string'" severity="error" variant="simple" size="small">
@@ -51,6 +52,10 @@
51
52
  type: Object,
52
53
  default: () => ({})
53
54
  },
55
+ errors: {
56
+ type: Object,
57
+ default: () => ({})
58
+ },
54
59
  propName: {
55
60
  type: String,
56
61
  default: ''
@@ -0,0 +1,230 @@
1
+ import { useToast } from 'primevue/usetoast'
2
+ import { usePath, live, useApi } from '@live-change/vue3-ssr'
3
+ import { ref, computed, inject, watch, getCurrentInstance } from 'vue'
4
+ import { synchronized, defaultData } from '@live-change/vue3-components'
5
+
6
+ import { propertiesValidationErrors } from './validation.js'
7
+
8
+ import { cyrb128 } from './utils.js'
9
+
10
+ export default async function actionData(options) {
11
+ if(!options) throw new Error('options must be provided')
12
+
13
+ const {
14
+ parameters,
15
+ initialValue = {},
16
+
17
+ service: serviceName,
18
+ action: actionName,
19
+
20
+ draft = true,
21
+ recursive = true,
22
+ debounce = 600,
23
+ timeField = 'lastUpdate',
24
+
25
+ doneToast = "Action done",
26
+ errorToast = "Action error",
27
+ resetToast = "Reset done",
28
+ savedDraftToast = "Draft saved",
29
+ discardedDraftToast = "Draft discarded",
30
+
31
+ onDone = ({ result, data }) => {},
32
+ onError = ({ error, data }) => { throw error } ,
33
+ onReset = () => {},
34
+
35
+ onDraftSaved = () => {},
36
+ onDraftDiscarded = () => {},
37
+
38
+ appContext = getCurrentInstance().appContext,
39
+
40
+ toast = useToast(options.appContext),
41
+ path = usePath(options.appContext),
42
+ api = useApi(options.appContext),
43
+ workingZone = inject('workingZone', options.appContext),
44
+
45
+ } = options
46
+
47
+ if(!serviceName || !actionName) throw new Error('service and action must be defined')
48
+
49
+ const service = api.services[serviceName]
50
+ if(!service) throw new Error('service must be defined in options')
51
+ const action = service.actions[actionName]
52
+ if(!action) throw new Error('action must be defined in options')
53
+
54
+ let draftIdParts = []
55
+ let idKey = null
56
+ for(const [parameterName, parameter] of Object.entries(parameters || {})) {
57
+ if(parameter.type === 'id') {
58
+ idKey = parameterName
59
+ } else {
60
+ draftIdParts.push(parameterName)
61
+ }
62
+ }
63
+
64
+ let draftId = (idKey ? parameters[idKey]
65
+ : draftIdParts.map(key => JSON.stringify(parameters[key])).join('_')) ?? 'any'
66
+ if(draftId.length > 16) {
67
+ draftId = cyrb128(draftId).slice(0, 16)
68
+ }
69
+
70
+ const draftIdentifiers = {
71
+ actionType: serviceName, action: 'action', targetType: actionName, target: draftId || 'any'
72
+ }
73
+
74
+ const draftDataPath = (draft && path.draft.myDraft(draftIdentifiers)) || null
75
+
76
+ const createOrUpdateDraftAction = draft && api.actions.draft.setOrUpdateMyDraft
77
+ const removeDraftAction = draft && api.actions.draft.resetMyDraft
78
+
79
+ const propertiesServerErrors = ref({})
80
+ const lastSentData = ref(null)
81
+ const formData = ref(JSON.parse(JSON.stringify(initialValue)))
82
+ const done = ref(false)
83
+
84
+ let commandPromise = null
85
+ const submitting = ref(false)
86
+ async function submitData(data){
87
+ const requestData = JSON.parse(JSON.stringify({ ...data, ...parameters }))
88
+ if(commandPromise) await commandPromise // wait for previous save
89
+ submitting.value = true
90
+ commandPromise = (async () => {
91
+ propertiesServerErrors.value = {}
92
+ lastSentData.value = JSON.parse(JSON.stringify(requestData))
93
+ console.log("ACTION COMMAND DATA", requestData)
94
+ try {
95
+ const result = await action(requestData)
96
+ done.value = true
97
+ await onDone(result)
98
+ return result
99
+ } catch(e) {
100
+ console.log("ACTION COMMAND ERROR", e)
101
+ if(e.properties) {
102
+ propertiesServerErrors.value = e.properties
103
+ } else {
104
+ await onError(e)
105
+ }
106
+ return Error
107
+ } finally {
108
+ submitting.value = false
109
+ commandPromise = null
110
+ }
111
+ })()
112
+ if(workingZone) workingZone.addPromise('command:'+serviceName+':'+actionName, commandPromise)
113
+ return await commandPromise
114
+ }
115
+
116
+ if(draft) {
117
+ const draftData = await live(draftDataPath)
118
+ function saveDraft(data){
119
+ return createOrUpdateDraftAction({ ...data, from: formData.value })
120
+ }
121
+ const source = computed(() => draftData.value?.data || initialValue)
122
+ const synchronizedData = synchronized({
123
+ source,
124
+ update: saveDraft,
125
+ updateDataProperty: 'data',
126
+ identifiers: draftIdentifiers,
127
+ recursive,
128
+ autoSave: true,
129
+ debounce,
130
+ timeField,
131
+ resetOnError: false,
132
+ onSave: () => {
133
+ onDraftSaved()
134
+ if(toast && savedDraftToast) toast.add({ severity: 'info', summary: savedDraftToast, life: 1500 })
135
+ },
136
+ onSaveError: (e) => {
137
+ console.error("DRAFT SAVE ERROR", e)
138
+ if(toast && saveDraftErrorToast)
139
+ toast.add({ severity: 'error', summary: saveDraftErrorToast, detail: e.message ?? e, life: 5000 })
140
+ }
141
+ })
142
+ const changed = computed(() =>
143
+ JSON.stringify(initialValue ?? {})
144
+ !== JSON.stringify({ ...synchronizedData.value.value, [timeField]: undefined })
145
+ )
146
+
147
+ const propertiesErrors = computed(() => propertiesValidationErrors(
148
+ synchronizedData.value.value, parameters, action.definition, lastSentData.value,
149
+ propertiesServerErrors.value, appContext))
150
+
151
+ async function submit() {
152
+ const result = await submitData(synchronizedData.value.value)
153
+ if(result === Error) return // silent return on error, because it's handled in onError
154
+ if(draftData.value) await removeDraftAction(draftIdentifiers)
155
+ onDone(result)
156
+ if(toast && doneToast) toast.add({ severity: 'success', summary: doneToast, life: 1500 })
157
+ }
158
+
159
+ async function discardDraft() {
160
+ const discardPromise = removeDraftAction(draftIdentifiers)
161
+ if(workingZone)
162
+ workingZone.addPromise('discardDraft:'+serviceName+':'+actionName, discardPromise)
163
+ await discardPromise
164
+ onDraftDiscarded()
165
+ if(toast && discardedDraftToast) toast.add({ severity: 'info', summary: discardedDraftToast, life: 1500 })
166
+ }
167
+
168
+ async function reset() {
169
+ const discardPromise = removeDraftAction(draftIdentifiers)
170
+ if(workingZone)
171
+ workingZone.addPromise('discardDraft:'+serviceName+':'+actionName, discardPromise)
172
+ await discardPromise
173
+ synchronizedData.value.value = editableSavedData.value || defaultData(model)
174
+ if(toast && discardedDraftToast) toast.add({ severity: 'info', summary: resetToast, life: 1500 })
175
+ done.value = false
176
+ onReset()
177
+ }
178
+
179
+ return {
180
+ parameters,
181
+ initialValue,
182
+ value: synchronizedData.value,
183
+ changed,
184
+ submit,
185
+ submitting,
186
+ reset,
187
+ action,
188
+ discardDraft,
189
+ propertiesErrors,
190
+ lastSentData,
191
+ draftChanged: synchronizedData.changed,
192
+ saveDraft: synchronizedData.save,
193
+ savingDraft: synchronizedData.saving,
194
+ done: done,
195
+ draft: draftData
196
+ }
197
+
198
+ } else {
199
+
200
+ async function submit() {
201
+ const result = await submitData(formData.value)
202
+ if(result === Error) return // silent return on error, because it's handled in onError
203
+ onSaved(result)
204
+ if(toast && doneToast) toast.add({ severity: 'success', summary: doneToast, life: 1500 })
205
+ }
206
+
207
+ const changed = computed(() =>
208
+ JSON.stringify(initialValue ?? {})
209
+ !== JSON.stringify(formData.value)
210
+ )
211
+
212
+ function reset() {
213
+ formData.value = JSON.parse(JSON.stringify(initialValue))
214
+ }
215
+
216
+ return {
217
+ parameters,
218
+ initialValue,
219
+ value: formData,
220
+ changed,
221
+ submit,
222
+ submitting,
223
+ done,
224
+ propertiesErrors,
225
+ reset
226
+ }
227
+
228
+ }
229
+
230
+ }
@@ -1,28 +1,11 @@
1
1
  import { useToast } from 'primevue/usetoast'
2
2
  import { usePath, live, useApi } from '@live-change/vue3-ssr'
3
- import { ref, computed, inject, watch } from 'vue'
3
+ import { ref, computed, inject, watch, getCurrentInstance } from 'vue'
4
4
  import { synchronized, defaultData } from '@live-change/vue3-components'
5
5
 
6
- function cyrb128(str) {
7
- let h1 = 1779033703, h2 = 3144134277,
8
- h3 = 1013904242, h4 = 2773480762;
9
- for (let i = 0, k; i < str.length; i++) {
10
- k = str.charCodeAt(i);
11
- h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
12
- h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
13
- h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
14
- h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
15
- }
16
- h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
17
- h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
18
- h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
19
- h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
20
- h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
21
- const data = new Uint32Array([h4>>>0, h3>>>0, h2>>>0, h1>>>0])
22
- // convert to base64
23
- const data8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
24
- return btoa(String.fromCharCode(...data8))
25
- }
6
+ import { propertiesValidationErrors } from './validation.js'
7
+
8
+ import { cyrb128 } from './utils.js'
26
9
 
27
10
  export default function editorData(options) {
28
11
  if(!options) throw new Error('options must be provided')
@@ -54,10 +37,13 @@ export default function editorData(options) {
54
37
  onSaveError = () => {},
55
38
  onCreated = (createResult) => {},
56
39
 
57
- toast = useToast(),
58
- path = usePath(),
59
- api = useApi(),
60
- workingZone = inject('workingZone')
40
+
41
+ appContext = getCurrentInstance().appContext,
42
+
43
+ toast = useToast(options.appContext),
44
+ path = usePath(options.appContext),
45
+ api = useApi(options.appContext),
46
+ workingZone = inject('workingZone', options.appContext),
61
47
 
62
48
  } = options
63
49
 
@@ -123,27 +109,39 @@ export default function editorData(options) {
123
109
  ))
124
110
  const source = computed(() => editableDraftData.value || editableSavedData.value || defaultData(model))
125
111
 
112
+ const propertiesServerErrors = ref({})
113
+ const lastUploadedData = ref(null)
114
+
126
115
  let savePromise = null
127
116
  const saving = ref(false)
128
117
  async function saveData(data){
129
118
  const requestData = {
130
119
  ...(updateDataProperty ? { [updateDataProperty]: data } : data),
131
120
  ...identifiers
132
- }
121
+ }
133
122
  if(savePromise) await savePromise // wait for previous save
134
123
  saving.value = true
135
124
  savePromise = (async () => {
125
+ propertiesServerErrors.value = {}
126
+ lastUploadedData.value = JSON.parse(JSON.stringify(data))
127
+ console.log("SAVE DATA", requestData)
136
128
  try {
137
129
  if(createOrUpdateAction) {
138
- return createOrUpdateAction(requestData)
130
+ return await createOrUpdateAction(requestData)
139
131
  }
140
132
  if(savedData.value) {
141
- return updateAction(requestData)
133
+ return await updateAction(requestData)
142
134
  } else {
143
135
  const createResult = await createAction(requestData)
144
136
  await onCreated(createResult)
145
137
  return createResult
146
138
  }
139
+ } catch(e) {
140
+ console.log("SAVE ERROR", e)
141
+ if(e.properties) {
142
+ propertiesServerErrors.value = e.properties
143
+ }
144
+ throw e
147
145
  } finally {
148
146
  saving.value = false
149
147
  savePromise = null
@@ -167,7 +165,6 @@ export default function editorData(options) {
167
165
  autoSave: true,
168
166
  debounce,
169
167
  timeField,
170
- isNew,
171
168
  resetOnError: false,
172
169
  onSave: () => {
173
170
  onDraftSaved()
@@ -183,12 +180,15 @@ export default function editorData(options) {
183
180
  JSON.stringify(editableSavedData.value ?? {})
184
181
  !== JSON.stringify({ ...synchronizedData.value.value, [timeField]: undefined })
185
182
  )
186
-
187
183
  const sourceChanged = computed(() =>
188
184
  JSON.stringify(draftData.value.from)
189
185
  !== JSON.stringify({ ...synchronizedData.value.value, [timeField]: undefined })
190
186
  )
191
187
 
188
+ const propertiesErrors = computed(() => propertiesValidationErrors(
189
+ synchronizedData.value.value, identifiers, model, lastUploadedData.value,
190
+ propertiesServerErrors.value, appContext))
191
+
192
192
  async function save() {
193
193
  const saveResult = await saveData(synchronizedData.value.value)
194
194
  if(draftData.value) await removeDraftAction(draftIdentifiers)
@@ -203,7 +203,7 @@ export default function editorData(options) {
203
203
  await discardPromise
204
204
  onDraftDiscarded()
205
205
  if(toast && discardedDraftToast) toast.add({ severity: 'info', summary: discardedDraftToast, life: 1500 })
206
- }
206
+ }
207
207
 
208
208
  async function reset() {
209
209
  const discardPromise = removeDraftAction(draftIdentifiers)
@@ -225,7 +225,7 @@ export default function editorData(options) {
225
225
  discardDraft,
226
226
  model,
227
227
  isNew,
228
- resetOnError: false,
228
+ propertiesErrors,
229
229
  draftChanged: synchronizedData.changed,
230
230
  saveDraft: synchronizedData.save,
231
231
  savingDraft: synchronizedData.saving,
@@ -255,6 +255,10 @@ export default function editorData(options) {
255
255
  }
256
256
  })
257
257
 
258
+ const propertiesErrors = computed(() => propertiesValidationErrors(
259
+ synchronizedData.value.value, identifiers, model, lastUploadedData.value,
260
+ propertiesServerErrors.value, appContext))
261
+
258
262
  async function reset() {
259
263
  synchronizedData.value.value = editableSavedData.value || defaultData(model)
260
264
  if(toast && discardedDraftToast) toast.add({ severity: 'info', summary: resetToast, life: 1500 })
@@ -269,6 +273,7 @@ export default function editorData(options) {
269
273
  saving: synchronizedData.saving,
270
274
  saved: savedData,
271
275
  savedPath: savedDataPath,
276
+ propertiesErrors,
272
277
  reset,
273
278
  model,
274
279
  }
@@ -0,0 +1,20 @@
1
+ export function cyrb128(str) {
2
+ let h1 = 1779033703, h2 = 3144134277,
3
+ h3 = 1013904242, h4 = 2773480762;
4
+ for (let i = 0, k; i < str.length; i++) {
5
+ k = str.charCodeAt(i);
6
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
7
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
8
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
9
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
10
+ }
11
+ h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
12
+ h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
13
+ h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
14
+ h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
15
+ h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
16
+ const data = new Uint32Array([h4>>>0, h3>>>0, h2>>>0, h1>>>0])
17
+ // convert to base64
18
+ const data8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
19
+ return btoa(String.fromCharCode(...data8))
20
+ }
@@ -0,0 +1,45 @@
1
+ import { validateData } from "@live-change/vue3-components"
2
+
3
+ export function propertiesValidationErrors(rootValue, parameters, definition, lastData, propertiesServerErrors, appContext) {
4
+ const currentValue = {
5
+ ...parameters,
6
+ ...rootValue,
7
+ }
8
+
9
+ console.log("propertiesValidationErrors", rootValue, parameters, definition,
10
+ lastData, propertiesServerErrors, appContext)
11
+
12
+ const validationResult = validateData(definition, currentValue, 'validation', appContext,
13
+ '', rootValue, true)
14
+
15
+ const softValidationResult = validateData(definition, currentValue, 'softValidation', appContext,
16
+ '', rootValue, true)
17
+
18
+ const serverValidationResult = {}
19
+ if(propertiesServerErrors) {
20
+ console.log("propertiesServerErrors", propertiesServerErrors, lastData, rootValue)
21
+ for(const propPathString in propertiesServerErrors) {
22
+ const propPath = propPathString.split('.')
23
+ let last = lastData
24
+ let current = rootValue
25
+ for(const prop of propPath) {
26
+ last = last?.[prop] ?? null
27
+ current = current?.[prop] ?? null
28
+ }
29
+ if(JSON.stringify(last) === JSON.stringify(current)) {
30
+ serverValidationResult[propPathString] = propertiesServerErrors[propPathString]
31
+ }
32
+ }
33
+ }
34
+
35
+ console.log("currentValue", currentValue)
36
+ console.log("validationResult", validationResult, softValidationResult)
37
+ console.log("serverValidationResult", serverValidationResult)
38
+
39
+ return {
40
+ ...softValidationResult?.propertyErrors,
41
+ ...validationResult?.propertyErrors,
42
+ ...serverValidationResult,
43
+ }
44
+
45
+ }
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div class="w-full lg:w-8/12 md:w-11/12">
3
+ <div class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border">
4
+ <div class="text-xl mb-2">
5
+ Service <strong>{{ serviceName }}</strong>
6
+ </div>
7
+ <div class="text-2xl mb-4">
8
+ Action <strong>{{ actionName }}</strong>
9
+ </div>
10
+
11
+ <ActionForm
12
+ :service="serviceName"
13
+ :action="actionName"
14
+ @done="handleDone"
15
+ />
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script setup>
21
+ import { ref, computed, onMounted, defineProps, toRefs } from 'vue'
22
+ import ActionForm from '../components/crud/ActionForm.vue'
23
+
24
+ const props = defineProps({
25
+ serviceName: {
26
+ type: String,
27
+ required: true
28
+ },
29
+ actionName: {
30
+ type: String,
31
+ required: true
32
+ }
33
+ })
34
+ const { serviceName, actionName } = toRefs(props)
35
+
36
+ import { useApi } from '@live-change/vue3-ssr'
37
+ const api = useApi()
38
+
39
+ function handleDone(result) {
40
+ console.log('Action executed:', result)
41
+ }
42
+ </script>
43
+
44
+ <style scoped>
45
+ </style>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div class="w-full lg:w-8/12 md:w-11/12">
3
+ <div v-for="serviceWithActions of editableActionsByService"
4
+ class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border">
5
+ <div class="text-xl mb-2">
6
+ Service <strong>{{ serviceWithActions.name }}</strong>
7
+ </div>
8
+ <div v-for="action of serviceWithActions.actions" class="mb-2 ml-4">
9
+ <div class="mb-1 flex flex-row flex-wrap items-center justify-between">
10
+ <div class="text-xl flex flex-row items-center mr-6">
11
+ <strong>{{ action.name }}</strong>
12
+ <span class="mx-1">action</span>
13
+ </div>
14
+ <div class="mt-2 md:mt-0">
15
+ <router-link :to="actionRoute(serviceWithActions.name, action)" class="no-underline">
16
+ <Button icon="pi pi-play" severity="success" :label="'Execute '+action.name" />
17
+ </router-link>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup>
26
+ import { ref, computed, onMounted, defineProps, toRefs } from 'vue'
27
+ import Button from 'primevue/button'
28
+
29
+ const props = defineProps({
30
+ serviceName: {
31
+ type: String,
32
+ default: undefined
33
+ },
34
+ })
35
+ const { serviceName } = toRefs(props)
36
+
37
+ import { useApi } from '@live-change/vue3-ssr'
38
+ const api = useApi()
39
+
40
+ const editableActionsByService = computed(() => {
41
+ const results = []
42
+ for(const [currentServiceName, service] of Object.entries(api.services)) {
43
+ if(serviceName.value && currentServiceName !== serviceName.value) continue
44
+ const actions = Object.entries(service.actions || {})
45
+ .filter(([_, action]) => action.autoForm !== false)
46
+ .map(([name, action]) => ({
47
+ name,
48
+ action
49
+ }))
50
+ if(actions.length === 0) continue
51
+ const result = {
52
+ name: currentServiceName,
53
+ actions
54
+ }
55
+ results.push(result)
56
+ }
57
+ return results
58
+ })
59
+
60
+ function actionRoute(serviceName, action) {
61
+ return {
62
+ name: 'auto-form:action',
63
+ params: {
64
+ serviceName,
65
+ actionName: action.name
66
+ }
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <style scoped>
72
+ </style>
@@ -8,6 +8,12 @@ export function autoFormRoutes(config = {}) {
8
8
  props: true
9
9
  }),
10
10
 
11
+ route({
12
+ name: 'auto-form:actions', path: prefix + '/actions/:serviceName?', meta: { },
13
+ component: () => import("./pages/Actions.vue"),
14
+ props: true
15
+ }),
16
+
11
17
  route({
12
18
  name: 'auto-form:editor', path: prefix + '/editor/:serviceName/:modelName/:identifiers*', meta: { },
13
19
  component: () => import("./pages/Editor.vue"),
@@ -26,6 +32,12 @@ export function autoFormRoutes(config = {}) {
26
32
  props: true
27
33
  }),
28
34
 
35
+ route({
36
+ name: 'auto-form:action', path: prefix + '/action/:serviceName/:actionName', meta: { },
37
+ component: () => import("./pages/Action.vue"),
38
+ props: true
39
+ }),
40
+
29
41
  ]
30
42
  }
31
43
 
package/index.js CHANGED
@@ -10,6 +10,8 @@ export * from './front/src/components/form/inputConfigInjection.js'
10
10
  import editorData from './front/src/logic/editorData.js'
11
11
  export { editorData }
12
12
 
13
+ import actionData from './front/src/logic/actionData.js'
14
+ export { actionData }
13
15
 
14
16
  import AutoView from './front/src/components/view/AutoView.vue'
15
17
  import AutoViewField from './front/src/components/view/AutoViewField.vue'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/frontend-auto-form",
3
- "version": "0.9.77",
3
+ "version": "0.9.79",
4
4
  "scripts": {
5
5
  "memDev": "node server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
@@ -22,16 +22,16 @@
22
22
  "type": "module",
23
23
  "dependencies": {
24
24
  "@fortawesome/fontawesome-free": "^6.7.2",
25
- "@live-change/cli": "^0.9.77",
26
- "@live-change/dao": "^0.9.77",
27
- "@live-change/dao-vue3": "^0.9.77",
28
- "@live-change/dao-websocket": "^0.9.77",
29
- "@live-change/framework": "^0.9.77",
30
- "@live-change/image-frontend": "^0.9.77",
31
- "@live-change/image-service": "^0.9.77",
32
- "@live-change/session-service": "^0.9.77",
33
- "@live-change/vue3-components": "^0.9.77",
34
- "@live-change/vue3-ssr": "^0.9.77",
25
+ "@live-change/cli": "^0.9.79",
26
+ "@live-change/dao": "^0.9.79",
27
+ "@live-change/dao-vue3": "^0.9.79",
28
+ "@live-change/dao-websocket": "^0.9.79",
29
+ "@live-change/framework": "^0.9.79",
30
+ "@live-change/image-frontend": "^0.9.79",
31
+ "@live-change/image-service": "^0.9.79",
32
+ "@live-change/session-service": "^0.9.79",
33
+ "@live-change/vue3-components": "^0.9.79",
34
+ "@live-change/vue3-ssr": "^0.9.79",
35
35
  "@vueuse/core": "^12.3.0",
36
36
  "codeceptjs-assert": "^0.0.5",
37
37
  "compression": "^1.7.5",
@@ -52,7 +52,7 @@
52
52
  "vue3-scroll-border": "0.1.6"
53
53
  },
54
54
  "devDependencies": {
55
- "@live-change/codeceptjs-helper": "^0.9.77",
55
+ "@live-change/codeceptjs-helper": "^0.9.79",
56
56
  "codeceptjs": "^3.6.10",
57
57
  "generate-password": "1.7.1",
58
58
  "playwright": "1.49.1",
@@ -63,5 +63,5 @@
63
63
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
64
64
  "license": "ISC",
65
65
  "description": "",
66
- "gitHead": "5998bcbd97bda91829bcf57b23e938d95d431144"
66
+ "gitHead": "68d2965e5a3b780063a5d0f9ce71d911fd1fb6be"
67
67
  }