@live-change/frontend-auto-form 0.9.85 → 0.9.87

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>
@@ -7,10 +7,41 @@
7
7
  Action <strong>{{ action }}</strong>
8
8
  </div>
9
9
 
10
- <form @submit="handleSubmit" @reset="handleReset">
10
+ <div v-if="task">
11
+ <Task :task="rootTask" :tasks="tasksData" />
12
+ <pre>rootTask = {{ rootTask }}</pre>
13
+ </div>
14
+ <div v-else-if="resultWithType">
15
+ <div class="text-xl mb-2">
16
+ Result:
17
+ </div>
18
+ <div class="text-sm text-gray-500">
19
+ <pre>{{ resultWithType }}</pre>
20
+ </div>
21
+ </div>
22
+ <form v-else @submit="handleSubmit" @reset="handleReset">
11
23
  <div class="flex flex-col gap-4">
24
+
25
+ <div v-for="[name, parameter] in Object.entries(actionFormData.parameters)"
26
+ class="flex flex-col mb-3">
27
+ <template v-if="!name.endsWith('Type')">
28
+ <div class="min-w-[8rem] font-medium">{{ actionFormData.action.definition.properties[name].label ?? name }}</div>
29
+ <div>
30
+ <InjectedObjectIndentification v-if="actionFormData.parameters[name+'Type']
31
+ ?? actionFormData.action.definition.properties[name]?.type
32
+ ?? actionFormData.action.definition.properties[name]?.type.split('_').length > 1"
33
+ :type="actionFormData.parameters[name+'Type']
34
+ ?? actionFormData.action.definition.properties[name]?.type"
35
+ :object="actionFormData.parameters[name]"
36
+ />
37
+ <pre v-else>parameter</pre>
38
+ </div>
39
+ </template>
40
+ </div>
41
+
12
42
  <auto-editor
13
43
  :definition="actionFormData.action.definition"
44
+ :editableProperties="actionFormData.editableProperties"
14
45
  v-model="actionFormData.value"
15
46
  :rootValue="actionFormData.value"
16
47
  :errors="actionFormData.propertiesErrors"
@@ -49,10 +80,14 @@
49
80
 
50
81
  <script setup>
51
82
 
52
- import { ref, computed, onMounted, defineProps, defineEmits, toRefs } from 'vue'
83
+ import { ref, computed, onMounted, defineProps, defineEmits, toRefs, watch } from 'vue'
53
84
  import Button from 'primevue/button'
54
85
  import AutoEditor from '../form/AutoEditor.vue'
55
86
  import ActionButtons from './ActionButtons.vue'
87
+ import { Task } from '@live-change/task-frontend'
88
+
89
+ import InjectedObjectIndentification from './InjectedObjectIndentification.vue'
90
+
56
91
  import { useToast } from 'primevue/usetoast'
57
92
  const toast = useToast()
58
93
 
@@ -68,34 +103,128 @@
68
103
  i18n: {
69
104
  type: String,
70
105
  default: ''
106
+ },
107
+ initialValue: {
108
+ type: Object,
109
+ default: () => ({})
110
+ },
111
+ parameters: {
112
+ type: Object,
113
+ default: () => ({})
71
114
  }
72
115
  })
73
116
 
74
- const { service, action, i18n } = toRefs(props)
117
+ const { service, action, i18n, initialValue, parameters } = toRefs(props)
75
118
 
76
119
  const emit = defineEmits(['done', 'error'])
77
120
 
78
- import { useApi } from '@live-change/vue3-ssr'
121
+ import { useApi, usePath, live } from '@live-change/vue3-ssr'
79
122
  const api = useApi()
123
+ const path = usePath()
124
+
125
+ import { useRouter } from 'vue-router'
126
+ const router = useRouter()
80
127
 
81
128
  import { actionData } from '@live-change/frontend-auto-form'
82
129
 
83
- const actionFormData = await actionData({
130
+ const resultWithType = ref(null)
131
+ const task = ref(null)
132
+
133
+ const tasksPath = computed(() => task.value && path.task.tasksByRoot({
134
+ root: task.value,
135
+ rootType: 'task_Task'
136
+ }))
137
+
138
+ const actionFormDataPromise = actionData({
84
139
  service: service.value,
85
140
  action: action.value,
86
- i18n: i18n.value
141
+ i18n: i18n.value,
142
+ initialValue: initialValue.value,
143
+ parameters: parameters.value,
144
+ onDone: handleDone
145
+ })
146
+
147
+ const [
148
+ actionFormData,
149
+ tasksData
150
+ ] = await Promise.all([
151
+ actionFormDataPromise,
152
+ live(tasksPath)
153
+ ])
154
+
155
+ const returnType = computed(() => {
156
+ const returnType = actionFormData.action.definition.returns
157
+ return returnType.type
87
158
  })
88
159
 
89
- const handleSubmit = (ev) => {
160
+ function handleSubmit(ev) {
90
161
  ev.preventDefault()
91
162
  actionFormData.submit()
92
163
  }
93
164
 
94
- const handleReset = (ev) => {
165
+ function handleReset(ev) {
95
166
  ev.preventDefault()
96
167
  actionFormData.reset()
97
168
  }
98
169
 
170
+ function handleDone(result) {
171
+ console.log('handleDone', result)
172
+ handleResult(result, returnType.value)
173
+ }
174
+
175
+ function handleResult(result, type) {
176
+ console.log('handleResult', result, type)
177
+ task.value = null
178
+ resultWithType.value = null
179
+ if(type === 'task_Task') {
180
+ task.value = result
181
+ } else if(type.split('_').length === 1) {
182
+ if(typeof result === 'object') {
183
+ resultWithType.value = {
184
+ type: type,
185
+ result: result
186
+ }
187
+ } else { /// redirect to object view
188
+ router.push({
189
+ name: 'auto-form:view',
190
+ params: {
191
+ serviceName: service.value,
192
+ modelName: type.split('_')[0],
193
+ identifiers: [result]
194
+ }
195
+ })
196
+ }
197
+ } else {
198
+ resultWithType.value = {
199
+ type: type,
200
+ result: result
201
+ }
202
+ }
203
+ }
204
+
205
+ const rootTask = computed(() => {
206
+ if(task.value && tasksData.value) {
207
+ return tasksData.value.find(t => t.id === task.value)
208
+ }
209
+ })
210
+
211
+ const rootTaskDone = computed(() => {
212
+ if(rootTask.value) {
213
+ return rootTask.value.state === 'done'
214
+ }
215
+ return false
216
+ })
217
+
218
+ /* watch(rootTaskDone, (done) => {
219
+ if(done) {
220
+ const taskType = rootTask.value.type
221
+ const taskService = rootTask.value.service
222
+ const taskDefinition = api.serviceDefinition(taskService).tasks[taskType]
223
+ handleResult(rootTask.value.result, taskDefinition.returns.type)
224
+ }
225
+ }, { immediate: true }) */
226
+
227
+
99
228
  </script>
100
229
 
101
230
  <style scoped>
@@ -98,7 +98,7 @@
98
98
  params: {
99
99
  serviceName: service.value,
100
100
  modelName: model.value,
101
- identifiers: Object.values(objectIdentifiers(objectData.value))
101
+ id: objectData.value?.to ?? objectData.value?.id
102
102
  }
103
103
  }
104
104
  })
@@ -1,8 +1,22 @@
1
1
  <template>
2
- <template v-if="typeof identificationConfig === 'string'">
2
+ <template v-if="identificationParts.length > 0">
3
3
  <span>
4
- <i :class="[icon, 'mr-2']" style="font-size: 0.9em;"></i>{{ objectData[identificationConfig] }}
5
- </span>
4
+ <i v-if="modelDefinition.icon" :class="[modelDefinition.icon, 'mr-2']" style="font-size: 0.9em;"></i>
5
+ <template v-for="(part, index) in identificationParts">
6
+ <i v-if="part.icon" :class="[part.icon, 'mr-2']" style="font-size: 0.9em;"></i>
7
+ <span :class="{ 'mr-2': index < identificationParts.length - 1 }">
8
+ <span v-if="part.isObject">
9
+ <InjectedObjectIndentification :type="part.type" :object="part.value" />
10
+ </span>
11
+ <span v-else-if="part.field">
12
+ {{ part.value }}
13
+ </span>
14
+ <span v-else>
15
+ {{ part.text ?? '' }}
16
+ </span>
17
+ </span>
18
+ </template>
19
+ </span>
6
20
  </template>
7
21
  <template v-else>
8
22
  <span>
@@ -13,7 +27,7 @@
13
27
 
14
28
  <script setup>
15
29
 
16
- import { ref, computed, onMounted, defineProps, defineEmits, toRefs } from 'vue'
30
+ import { ref, computed, onMounted, defineProps, defineEmits, toRefs, defineAsyncComponent } from 'vue'
17
31
 
18
32
  const props = defineProps({
19
33
  objectType: {
@@ -70,9 +84,63 @@
70
84
  })
71
85
  })
72
86
 
87
+ function getDefinitionProperty(path) {
88
+ const parts = path.split('.')
89
+ let current = modelDefinition.value
90
+ for(const part of parts) {
91
+ if(part === '*') {
92
+ return current?.items ?? current?.of
93
+ } else {
94
+ current = current?.properties?.[part]
95
+ }
96
+ }
97
+ return current
98
+ }
99
+
100
+ const InjectedObjectIndentification = defineAsyncComponent(() => import('./InjectedObjectIndentification.vue'))
101
+
73
102
  const loadedObjectData = await live(objectDataPath)
74
103
  const objectData = computed(() => data.value || loadedObjectData.value)
75
104
 
105
+ function getObjectData(path) {
106
+ const parts = path.split('.')
107
+ let current = objectData.value
108
+ for(const part of parts) {
109
+ current = current[part]
110
+ }
111
+ return current
112
+ }
113
+
114
+ const identificationParts = computed(() => {
115
+ const config = identificationConfig.value
116
+ if(!config) return []
117
+ const configArray = Array.isArray(config) ? config : [config]
118
+ return configArray.map(fieldConfig => {
119
+ if(typeof fieldConfig === 'string') {
120
+ const field = getDefinitionProperty(fieldConfig)
121
+ if(field) {
122
+ const isObject = field.type.indexOf('_') > 0
123
+ return { field: fieldConfig, isObject, type: field.type }
124
+ } else {
125
+ return { text: fieldConfig }
126
+ }
127
+ } else if(typeof fieldConfig === 'object') {
128
+ return fieldConfig
129
+ } else {
130
+ throw new Error('Unknown identification config: ' + JSON.stringify(fieldConfig))
131
+ }
132
+ }).map(part => {
133
+ if(part.type === 'any') {
134
+ part.type = getObjectData(part.field + 'Type')
135
+ part.isObject = (part.type && part.type.indexOf('_') > 0) || false
136
+ }
137
+ if(part.field) {
138
+ part.value = getObjectData(part.field)
139
+ }
140
+ return part
141
+ })
142
+ })
143
+
76
144
 
77
145
  const icon = computed(() => {
78
146
  if(modelDefinition.value?.iconProperty) return objectData[modelDefinition.value?.iconProperty]
@@ -45,7 +45,7 @@
45
45
  })
46
46
  const { type, object, data, link, inline } = toRefs(props)
47
47
 
48
- import AutoObjectIdentification from './DefaultObjectIdentification.vue'
48
+ import DefaultObjectIdentification from './DefaultObjectIdentification.vue'
49
49
 
50
50
  import { useApi, usePath, live } from '@live-change/vue3-ssr'
51
51
  const api = useApi()
@@ -64,7 +64,7 @@
64
64
  type: type.value,
65
65
  service: service.value,
66
66
  model: model.value
67
- }, AutoObjectIdentification)
67
+ }, DefaultObjectIdentification)
68
68
  )
69
69
 
70
70
  const modelDefinition = computed(() => {
@@ -115,7 +115,7 @@
115
115
  params: {
116
116
  serviceName: service.value,
117
117
  modelName: model.value,
118
- identifiers: Object.values(objectIdentifiers(objectData.value))
118
+ id: objectData.value?.to ?? objectData.value?.id
119
119
  }
120
120
  }
121
121
  })
@@ -19,17 +19,24 @@
19
19
  </div>
20
20
  </div>
21
21
 
22
- <pre>parentObjects = {{ parentObjects }}</pre>
22
+ <!-- <pre>parentObjects = {{ parentObjects }}</pre>
23
23
  <pre>scopesPath = {{ scopesPath }}</pre>
24
- <pre>scopes = {{ scopes }}</pre>
24
+ <pre>scopes = {{ scopes }}</pre> -->
25
25
 
26
26
  <div class="">
27
27
  Service <strong>{{ service }}</strong>
28
28
  </div>
29
- <div class="text-2xl mb-6">
30
- <span v-if="isNew">Create </span>
31
- <span v-else>Edit </span>
32
- <strong>{{ model }}</strong>
29
+ <div class="flex flex-row flex-wrap justify-between align-items-top">
30
+ <div class="text-2xl mb-6">
31
+ <span v-if="isNew">Create </span>
32
+ <span v-else>Edit </span>
33
+ <strong>{{ model }}</strong>
34
+ </div>
35
+ <div v-if="!isNew" class="flex flex-row flex-wrap justify-between align-items-top gap-2">
36
+ <router-link :to="viewRoute">
37
+ <Button label="View" icon="pi pi-eye" class="p-button mb-6" />
38
+ </router-link>
39
+ </div>
33
40
  </div>
34
41
 
35
42
  <form v-if="editor" @submit="handleSave" @reset="handleReset">
@@ -162,11 +169,21 @@
162
169
  console.log("SAVED", saveResult, isNew.value, editor.value.isNew)
163
170
  if(saveResult && isNew.value && editor.value.isNew) {
164
171
  emit('created', saveResult)
172
+ } else {
173
+ emit('saved', saveResult)
165
174
  }
166
- emit('saved', saveResult)
167
175
  }
168
176
 
169
- const scopesPath = computed(() => path.scope.objectScopes({
177
+ const viewRoute = computed(() => editor.value?.saved && ({
178
+ name: 'auto-form:view',
179
+ params: {
180
+ serviceName: service.value,
181
+ modelName: model.value,
182
+ id: editor.value.saved?.value?.to ?? editor.value.saved?.value?.id
183
+ }
184
+ }))
185
+
186
+ const scopesPath = computed(() => parentObjects.value[0] && path.scope.objectScopes({
170
187
  objectType: parentObjects.value[0].objectType, /// TODO: support multiple parent objects!
171
188
  object: parentObjects.value[0].object
172
189
  }))
@@ -27,7 +27,7 @@
27
27
  :key="JSON.stringify(modelsPathRangeConfig)+index"
28
28
  :pathFunction="modelsPathRangeFunction"
29
29
  :canLoadTop="false" canDropBottom
30
- loadBottomSensorSize="4000px" dropBottomSensorSize="3000px">
30
+ loadBottomSensorSize="4000px" dropBottomSensorSize="7000px">
31
31
  <template #empty>
32
32
  <div class="text-xl text-surface-800 dark:text-surface-50 my-1 mx-4">
33
33
  No <strong>{{ pluralize(model[0].toLowerCase() + model.slice(1)) }}</strong> found.
@@ -43,7 +43,7 @@
43
43
  class="text-xl"
44
44
  />
45
45
  </router-link>
46
- <div class="flex flex-row">
46
+ <div class="flex flex-row ml-4">
47
47
  <router-link :to="viewRoute(object)" class="no-underline">
48
48
  <Button icon="pi pi-eye" severity="primary" label="View" class="mr-2" />
49
49
  </router-link>
@@ -70,9 +70,11 @@
70
70
  </div>
71
71
  </div>
72
72
 
73
- <div v-if="modelDefinition.crud?.create" class="mt-2 flex flex-row justify-end mr-2">
74
- <router-link :to="createRoute" class="no-underline2">
75
- <Button icon="pi pi-plus" :label="'Create new '+model" />
73
+ <div v-if="modelDefinition.crud?.create" class="mt-2 flex flex-row justify-end mr-2 gap-2">
74
+ <router-link v-for="createButton in createButtons" :key="JSON.stringify(createButton.identifiers)"
75
+ :to="createButton.route" class="no-underline2">
76
+ <Button v-if="createButton.as" icon="pi pi-plus" :label="'Create new '+model + ' as '+createButton.as" />
77
+ <Button v-else icon="pi pi-plus" :label="'Create new '+model" />
76
78
  </router-link>
77
79
  </div>
78
80
 
@@ -198,11 +200,11 @@
198
200
 
199
201
  function editRoute(object) {
200
202
  return {
201
- name: 'auto-form:editor',
203
+ name: 'auto-form:edit',
202
204
  params: {
203
205
  serviceName: service.value,
204
206
  modelName: model.value,
205
- identifiers: Object.values(objectIdentifiers(object))
207
+ id: object.to ?? object.id
206
208
  }
207
209
  }
208
210
  }
@@ -213,23 +215,62 @@
213
215
  params: {
214
216
  serviceName: service.value,
215
217
  modelName: model.value,
216
- identifiers: Object.values(objectIdentifiers(object))
218
+ id: object.to ?? object.id
217
219
  }
218
220
  }
219
221
  }
220
222
 
221
- const createRoute = computed(() => {
222
- const identifiersObject = viewsArray?.value[0]?.identifiers
223
- if(!identifiersObject) return null
224
- const identifiers = Object.values(identifiersObject)
223
+ function createRoute(identifiersObject) {
224
+ const identifiersWithNames = Object.entries(identifiersObject).flat()
225
225
  return {
226
- name: 'auto-form:editor',
226
+ name: 'auto-form:create',
227
227
  params: {
228
228
  serviceName: service.value,
229
229
  modelName: model.value,
230
- identifiers
230
+ identifiersWithNames
231
231
  }
232
232
  }
233
+ }
234
+
235
+ const createButtons = computed(() => {
236
+ const model = modelDefinition.value
237
+ const results = []
238
+ for(const view of viewsArray.value) {
239
+ const identifiersObject = view?.identifiers
240
+ if(!identifiersObject) continue
241
+
242
+ const identifierNames = Object.keys(identifiersObject)
243
+ const fieldParameters = identifierNames.filter(name => model.properties[name])
244
+ const otherParameters = identifierNames.filter(name => !model.properties[name])
245
+
246
+ if(otherParameters.length > 0) {
247
+ const viewName = model.crud[view.name]
248
+ const viewDefinition = serviceDefinition.value.views[viewName]
249
+ for(const otherParameter of otherParameters) {
250
+ const otherParameterType = viewDefinition.properties[otherParameter].type
251
+ const modelParameters = Object.entries(model.properties)
252
+ .filter(([name, property]) => otherParameterType === property.type)
253
+ for(const [name, property] of modelParameters) {
254
+ const newIdentifiersObject = {
255
+ [name]: identifiersObject[otherParameter]
256
+ }
257
+ results.push({
258
+ identifiers: newIdentifiersObject,
259
+ as: name,
260
+ route: createRoute(newIdentifiersObject)
261
+ })
262
+ }
263
+ }
264
+ } else {
265
+ results.push({
266
+ identifiers: identifiersObject,
267
+ route: createRoute(identifiersObject)
268
+ })
269
+ }
270
+
271
+
272
+ }
273
+ return results
233
274
  })
234
275
 
235
276
  function deleteObject(event, object) {
@@ -189,11 +189,11 @@
189
189
 
190
190
  function editRoute(object) {
191
191
  return {
192
- name: 'auto-form:editor',
192
+ name: 'auto-form:edit',
193
193
  params: {
194
194
  serviceName: service.value,
195
195
  modelName: model.value,
196
- identifiers: Object.values(objectIdentifiers(object))
196
+ id: object.to ?? object.id
197
197
  }
198
198
  }
199
199
  }
@@ -204,17 +204,17 @@
204
204
  params: {
205
205
  serviceName: service.value,
206
206
  modelName: model.value,
207
- identifiers: Object.values(objectIdentifiers(object))
207
+ id: object.to ?? object.id
208
208
  }
209
209
  }
210
210
  }
211
211
 
212
212
  const createRoute = computed(() => ({
213
- name: 'auto-form:editor',
213
+ name: 'auto-form:create',
214
214
  params: {
215
215
  serviceName: service.value,
216
216
  modelName: model.value,
217
- identifiers: Object.values(objectIdentifiers(views.value[0].identifiers))
217
+ identifiersWithNames: Object.entries(objectIdentifiers(views.value[0].identifiers)).flat()
218
218
  }
219
219
  }))
220
220