@live-change/frontend-auto-form 0.9.84 → 0.9.86
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.
- package/front/src/components/crud/ActionForm.vue +137 -8
- package/front/src/components/crud/ModelEditor.vue +30 -12
- package/front/src/components/crud/ModelView.vue +88 -20
- package/front/src/components/schema/DataWithSchema.vue +41 -0
- package/front/src/components/schema/SchemaFromDefinition.vue +23 -0
- package/front/src/logic/actionData.js +8 -5
- package/front/src/logic/editorData.js +4 -4
- package/front/src/logic/relations.js +3 -0
- package/front/src/logic/schema.js +202 -0
- package/front/src/pages/Action.vue +9 -8
- package/front/src/pages/Editor.vue +13 -2
- package/front/src/router.js +6 -0
- package/index.js +6 -0
- package/package.json +13 -13
|
@@ -7,10 +7,41 @@
|
|
|
7
7
|
Action <strong>{{ action }}</strong>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
<
|
|
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
|
|
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
|
-
|
|
160
|
+
function handleSubmit(ev) {
|
|
90
161
|
ev.preventDefault()
|
|
91
162
|
actionFormData.submit()
|
|
92
163
|
}
|
|
93
164
|
|
|
94
|
-
|
|
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>
|
|
@@ -26,22 +26,30 @@
|
|
|
26
26
|
<div class="">
|
|
27
27
|
Service <strong>{{ service }}</strong>
|
|
28
28
|
</div>
|
|
29
|
-
<div class="
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
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">
|
|
36
|
-
<div v-for="identifier in modelDefinition.identifiers">
|
|
43
|
+
<div v-for="identifier in modelDefinition.identifiers">
|
|
37
44
|
<template v-if="(identifier.name ?? identifier).slice(-4) !== 'Type'">
|
|
38
|
-
<div v-if="identifiers[identifier]" class="flex flex-col mb-3">
|
|
45
|
+
<div v-if="identifiers[identifier.name ?? identifier]" class="flex flex-col mb-3">
|
|
39
46
|
<div class="min-w-[8rem] font-medium">{{ identifier.name ?? identifier }}</div>
|
|
40
47
|
<div class="">
|
|
41
|
-
<InjectedObjectIndentification
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
<InjectedObjectIndentification v-if="identifiers[(identifier.name ?? identifier)+'Type']
|
|
49
|
+
?? modelDefinition.properties[identifier.field ?? identifier]?.type"
|
|
50
|
+
:type="identifiers[(identifier.name ?? identifier)+'Type']
|
|
51
|
+
?? modelDefinition.properties[identifier.field ?? identifier]?.type"
|
|
52
|
+
:object="identifiers[identifier.name ?? identifier]"
|
|
45
53
|
/>
|
|
46
54
|
</div>
|
|
47
55
|
</div>
|
|
@@ -161,10 +169,20 @@
|
|
|
161
169
|
console.log("SAVED", saveResult, isNew.value, editor.value.isNew)
|
|
162
170
|
if(saveResult && isNew.value && editor.value.isNew) {
|
|
163
171
|
emit('created', saveResult)
|
|
164
|
-
}
|
|
165
|
-
|
|
172
|
+
} else {
|
|
173
|
+
emit('saved', saveResult)
|
|
174
|
+
}
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
const viewRoute = computed(() => ({
|
|
178
|
+
name: 'auto-form:editor',
|
|
179
|
+
params: {
|
|
180
|
+
serviceName: service.value,
|
|
181
|
+
modelName: model.value,
|
|
182
|
+
identifiers: Object.values(identifiers.value)
|
|
183
|
+
}
|
|
184
|
+
}))
|
|
185
|
+
|
|
168
186
|
const scopesPath = computed(() => path.scope.objectScopes({
|
|
169
187
|
objectType: parentObjects.value[0].objectType, /// TODO: support multiple parent objects!
|
|
170
188
|
object: parentObjects.value[0].object
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
|
-
<!-- <h4>identifiers</h4>
|
|
5
|
-
<pre>{{ identifiers }}</pre>
|
|
6
|
-
|
|
7
|
-
<h4>definition</h4>
|
|
8
|
-
<pre>{{ modelDefinition }}</pre>
|
|
9
|
-
|
|
10
|
-
<h4>object</h4>
|
|
11
|
-
<pre>{{ object }}</pre>-->
|
|
12
|
-
|
|
13
4
|
<div v-if="object">
|
|
14
5
|
<ObjectPath :objectType="service + '_' + model" :object="object.to ?? object.id" class="mb-6" />
|
|
15
6
|
|
|
@@ -28,7 +19,12 @@
|
|
|
28
19
|
class="ml-2"
|
|
29
20
|
/>
|
|
30
21
|
</div>
|
|
31
|
-
<
|
|
22
|
+
<div class="flex flex-row flex-wrap justify-between align-items-top gap-2">
|
|
23
|
+
<Button label="Access" icon="pi pi-key" class="p-button mb-6" @click="showAccessControl" />
|
|
24
|
+
<router-link :to="editRoute">
|
|
25
|
+
<Button label="Edit" icon="pi pi-pencil" class="p-button mb-6" />
|
|
26
|
+
</router-link>
|
|
27
|
+
</div>
|
|
32
28
|
</div>
|
|
33
29
|
|
|
34
30
|
<AutoView :value="object" :root-value="object" :i18n="i18n" :attributes="attributes"
|
|
@@ -36,6 +32,15 @@
|
|
|
36
32
|
|
|
37
33
|
</div>
|
|
38
34
|
|
|
35
|
+
<div v-if="connectedActions" class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border mb-6">
|
|
36
|
+
<div v-for="action of connectedActions" class="mb-6">
|
|
37
|
+
<pre>{{ action }}</pre>
|
|
38
|
+
<router-link :to="actionRoute(action)">
|
|
39
|
+
<Button :label="action.label" icon="pi pi-play" class="p-button mb-6" />
|
|
40
|
+
</router-link>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
39
44
|
<div v-for="preparedRelation of visibleObjectRelations" class="mb-6">
|
|
40
45
|
<ModelSingle :service="preparedRelation.service" :model="preparedRelation.model"
|
|
41
46
|
:views="preparedRelation.views">
|
|
@@ -93,7 +98,7 @@
|
|
|
93
98
|
|
|
94
99
|
<div class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border">
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
<!-- <pre>visibleRangeRelations = {{ visibleRangeRelations }}</pre>
|
|
97
102
|
<pre>preparedRelations = {{ preparedRelations }}</pre>
|
|
98
103
|
|
|
99
104
|
<div v-if="backwardRelations">
|
|
@@ -104,9 +109,13 @@
|
|
|
104
109
|
)
|
|
105
110
|
}}</pre>
|
|
106
111
|
</div>
|
|
112
|
+
<pre>accessControlRoles = {{ accessControlRoles }}</pre> -->
|
|
113
|
+
|
|
114
|
+
<pre>identifiers = {{ identifiers }}</pre>
|
|
107
115
|
|
|
116
|
+
<pre>definition = {{ modelDefinition }}</pre>
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
<pre>object = {{ object }}</pre>
|
|
110
119
|
|
|
111
120
|
</div>
|
|
112
121
|
|
|
@@ -173,6 +182,24 @@
|
|
|
173
182
|
return api.services?.[service.value]?.models?.[model.value]
|
|
174
183
|
})
|
|
175
184
|
|
|
185
|
+
const connectedActions = computed(() => {
|
|
186
|
+
const srcActions = modelDefinition.value?.connectedActions
|
|
187
|
+
if(!srcActions) return null
|
|
188
|
+
return Object.values(srcActions).map(action => {
|
|
189
|
+
const config = {
|
|
190
|
+
service: service.value,
|
|
191
|
+
...action
|
|
192
|
+
}
|
|
193
|
+
const actionDefinition = api.getServiceDefinition(config.service).actions[config.name]
|
|
194
|
+
const label = actionDefinition.label ?? action.label ?? actionDefinition.name
|
|
195
|
+
return {
|
|
196
|
+
...config,
|
|
197
|
+
definition: actionDefinition,
|
|
198
|
+
label
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
176
203
|
import { getForwardRelations, getBackwardRelations, anyRelationsTypes, prepareObjectRelations }
|
|
177
204
|
from '../../logic/relations.js'
|
|
178
205
|
const forwardRelations = computed(() => getForwardRelations(modelDefinition.value, () => true, api))
|
|
@@ -200,14 +227,18 @@
|
|
|
200
227
|
return prepareObjectRelations(objectType.value, object.value.to ?? object.value.id, api)
|
|
201
228
|
})
|
|
202
229
|
|
|
203
|
-
const visibleRangeRelations = computed(() => preparedRelations.value
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
const visibleRangeRelations = computed(() => preparedRelations.value
|
|
231
|
+
.filter(preparedRelation => !preparedRelation.singular)
|
|
232
|
+
.map(preparedRelation => {
|
|
233
|
+
const accessibleViews = preparedRelation.views.filter(view => preparedRelation.access.value[view.name])
|
|
234
|
+
if(accessibleViews.length === 0) return null
|
|
235
|
+
return {
|
|
236
|
+
...preparedRelation,
|
|
237
|
+
views: accessibleViews
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
.filter(x => x !== null)
|
|
241
|
+
)
|
|
211
242
|
|
|
212
243
|
const visibleObjectRelations = computed(() => preparedRelations.value.filter(preparedRelation => {
|
|
213
244
|
if(!preparedRelation.singular) return false
|
|
@@ -222,6 +253,43 @@
|
|
|
222
253
|
}
|
|
223
254
|
const accessControlRoles = computed(() => modelDefinition.value?.accessRoles ?? [])
|
|
224
255
|
|
|
256
|
+
const editRoute = computed(() => ({
|
|
257
|
+
name: 'auto-form:editor',
|
|
258
|
+
params: {
|
|
259
|
+
serviceName: service.value,
|
|
260
|
+
modelName: model.value,
|
|
261
|
+
identifiers: Object.values(identifiers.value)
|
|
262
|
+
}
|
|
263
|
+
}))
|
|
264
|
+
|
|
265
|
+
function actionRoute(action) {
|
|
266
|
+
const myType = service.value + '_' + model.value
|
|
267
|
+
const parameterName = action.objectParameter ??
|
|
268
|
+
Object.entries(action.definition.properties).find(([key, value]) => value.type === myType)?.[0] ??
|
|
269
|
+
Object.keys(action.definition.properties)?.[0]
|
|
270
|
+
|
|
271
|
+
if(parameterName) {
|
|
272
|
+
const parametersJson = JSON.stringify({
|
|
273
|
+
[parameterName]: object.value.to ?? object.value.id
|
|
274
|
+
})
|
|
275
|
+
return {
|
|
276
|
+
name: 'auto-form:actionParameters',
|
|
277
|
+
params: {
|
|
278
|
+
serviceName: action.service,
|
|
279
|
+
actionName: action.name,
|
|
280
|
+
parametersJson
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
name: 'auto-form:action',
|
|
286
|
+
params: {
|
|
287
|
+
serviceName: action.service,
|
|
288
|
+
actionName: action.name
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
225
293
|
</script>
|
|
226
294
|
|
|
227
295
|
<style scoped>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<pre>
|
|
3
|
+
#### {{ prefix }} Data Schema
|
|
4
|
+
|
|
5
|
+
{{ JSON.stringify(schema, null, 2) }}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
#### {{ prefix }} Data:
|
|
9
|
+
|
|
10
|
+
{{ JSON.stringify(clearData, null, 2) }}
|
|
11
|
+
</pre>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
|
|
16
|
+
import { computed, toRefs, defineProps, getCurrentInstance } from 'vue'
|
|
17
|
+
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
data: {
|
|
20
|
+
type: Object,
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
prefix: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: ''
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
const { data } = toRefs(props)
|
|
29
|
+
|
|
30
|
+
import { getSchemaFromData, cleanData } from "../../logic/schema.js"
|
|
31
|
+
|
|
32
|
+
const appContext = getCurrentInstance().appContext
|
|
33
|
+
|
|
34
|
+
const schema = computed(() => getSchemaFromData(data.value, appContext))
|
|
35
|
+
const clearData = computed(() => cleanData(data.value))
|
|
36
|
+
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style scoped>
|
|
40
|
+
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<pre>{{ schema }}</pre>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
|
|
7
|
+
import { schemaFromDefinition } from "../../logic/schema.js"
|
|
8
|
+
import { defineProps, toRefs, computed, getCurrentInstance } from "vue"
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
definition: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: true,
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const { definition } = toRefs(props)
|
|
18
|
+
|
|
19
|
+
const appContext = getCurrentInstance().appContext
|
|
20
|
+
|
|
21
|
+
const schema = computed(() => schemaFromDefinition(definition.value, undefined, undefined, appContext))
|
|
22
|
+
|
|
23
|
+
</script>
|
|
@@ -11,7 +11,7 @@ export default async function actionData(options) {
|
|
|
11
11
|
if(!options) throw new Error('options must be provided')
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
|
-
parameters,
|
|
14
|
+
parameters = {},
|
|
15
15
|
initialValue = {},
|
|
16
16
|
|
|
17
17
|
service: serviceName,
|
|
@@ -50,7 +50,10 @@ export default async function actionData(options) {
|
|
|
50
50
|
if(!service) throw new Error('service must be defined in options')
|
|
51
51
|
const action = service.actions[actionName]
|
|
52
52
|
if(!action) throw new Error('action must be defined in options')
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
const editableProperties = (action.definition.editableProperties ?? Object.keys(action.definition.properties))
|
|
55
|
+
.filter(key => !parameters[key])
|
|
56
|
+
|
|
54
57
|
let draftIdParts = []
|
|
55
58
|
let idKey = null
|
|
56
59
|
for(const [parameterName, parameter] of Object.entries(parameters || {})) {
|
|
@@ -152,7 +155,6 @@ export default async function actionData(options) {
|
|
|
152
155
|
const result = await submitData(synchronizedData.value.value)
|
|
153
156
|
if(result === Error) return // silent return on error, because it's handled in onError
|
|
154
157
|
if(draftData.value) await removeDraftAction(draftIdentifiers)
|
|
155
|
-
onDone(result)
|
|
156
158
|
if(toast && doneToast) toast.add({ severity: 'success', summary: doneToast, life: 1500 })
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -179,6 +181,7 @@ export default async function actionData(options) {
|
|
|
179
181
|
return {
|
|
180
182
|
parameters,
|
|
181
183
|
initialValue,
|
|
184
|
+
editableProperties,
|
|
182
185
|
value: synchronizedData.value,
|
|
183
186
|
changed,
|
|
184
187
|
submit,
|
|
@@ -200,7 +203,6 @@ export default async function actionData(options) {
|
|
|
200
203
|
async function submit() {
|
|
201
204
|
const result = await submitData(formData.value)
|
|
202
205
|
if(result === Error) return // silent return on error, because it's handled in onError
|
|
203
|
-
onSaved(result)
|
|
204
206
|
if(toast && doneToast) toast.add({ severity: 'success', summary: doneToast, life: 1500 })
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -214,8 +216,9 @@ export default async function actionData(options) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
return {
|
|
217
|
-
parameters,
|
|
219
|
+
parameters,
|
|
218
220
|
initialValue,
|
|
221
|
+
editableProperties,
|
|
219
222
|
value: formData,
|
|
220
223
|
changed,
|
|
221
224
|
submit,
|
|
@@ -40,10 +40,10 @@ export default function editorData(options) {
|
|
|
40
40
|
|
|
41
41
|
appContext = getCurrentInstance().appContext,
|
|
42
42
|
|
|
43
|
-
toast = useToast(options.appContext),
|
|
44
|
-
path = usePath(options.appContext),
|
|
45
|
-
api = useApi(options.appContext),
|
|
46
|
-
workingZone = inject('workingZone', options.appContext),
|
|
43
|
+
toast = useToast(options.appContext || getCurrentInstance().appContext),
|
|
44
|
+
path = usePath(options.appContext || getCurrentInstance().appContext),
|
|
45
|
+
api = useApi(options.appContext || getCurrentInstance().appContext),
|
|
46
|
+
workingZone = inject('workingZone', options.appContext || getCurrentInstance().appContext),
|
|
47
47
|
|
|
48
48
|
} = options
|
|
49
49
|
|
|
@@ -255,7 +255,10 @@ export function parentObjectsFromIdentifiers(identifiers, modelDefinition) {
|
|
|
255
255
|
const results = []
|
|
256
256
|
for(const [key, value] of Object.entries(identifiers)) {
|
|
257
257
|
if(key.endsWith('Type')) continue
|
|
258
|
+
const identifierDefinition = (modelDefinition.identifiers ?? []).find(i => i.name === key)
|
|
259
|
+
if(identifierDefinition && identifierDefinition.field === 'id') continue
|
|
258
260
|
const propertyDefinition = modelDefinition.properties[key]
|
|
261
|
+
if(!propertyDefinition) continue
|
|
259
262
|
const propertyType = propertyDefinition.type
|
|
260
263
|
if(propertyType === 'any') {
|
|
261
264
|
results.push({
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { sourceSymbol } from '@live-change/dao'
|
|
2
|
+
import { useApi } from '@live-change/vue3-ssr'
|
|
3
|
+
import { getCurrentInstance } from 'vue'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function mergeSchemas(s1, s2) {
|
|
7
|
+
return {
|
|
8
|
+
...s1,
|
|
9
|
+
...s2,
|
|
10
|
+
properties: {
|
|
11
|
+
...s1.properties,
|
|
12
|
+
...s2.properties
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addSchema(schemas, schema) {
|
|
18
|
+
for(let i = 0; i < schemas.length; i++) {
|
|
19
|
+
const s = schemas[i]
|
|
20
|
+
if(s.modelName === schema.modelName && s.serviceName === schema.serviceName) {
|
|
21
|
+
schemas[i] = mergeSchemas(s, schema)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
schemas.push(schema)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function schemaFromDefinition(definition, data, type, appContext = getCurrentInstance().appContext) {
|
|
29
|
+
if(!type) type = definition.type
|
|
30
|
+
if(type === 'Object') {
|
|
31
|
+
const properties = Object.fromEntries(
|
|
32
|
+
Object.entries(definition.properties).map(
|
|
33
|
+
([key, value]) => [key, schemaFromDefinition(value, data?.[key], undefined, appContext)])
|
|
34
|
+
)
|
|
35
|
+
for(const key in definition.properties) {
|
|
36
|
+
const prop = definition.properties[key]
|
|
37
|
+
if(key.endsWith('Type') && prop.type === 'type') {
|
|
38
|
+
const keyWithoutType = key.slice(0, -4)
|
|
39
|
+
properties[key] = {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: prop.enum,
|
|
42
|
+
description: `Type of ${keyWithoutType}`
|
|
43
|
+
}
|
|
44
|
+
properties[keyWithoutType] = {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: `Id of Object with type defined in ${key}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties,
|
|
53
|
+
description: definition.description
|
|
54
|
+
}
|
|
55
|
+
} else if(type === 'Array') {
|
|
56
|
+
const schema = {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: schemaFromDefinition(definition.items ?? definition.of, data?.[0], undefined, appContext),
|
|
59
|
+
description: definition.description
|
|
60
|
+
}
|
|
61
|
+
if(data) {
|
|
62
|
+
for(const item of data) {
|
|
63
|
+
extendSchema(schema.items, item, schema.items.modelName, schema.items.serviceName)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return schema
|
|
67
|
+
} else if(type === 'String') {
|
|
68
|
+
return {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: definition.description
|
|
71
|
+
}
|
|
72
|
+
} else if(type === 'Number') {
|
|
73
|
+
return {
|
|
74
|
+
type: 'number',
|
|
75
|
+
description: definition.description
|
|
76
|
+
}
|
|
77
|
+
} else if(type === 'Boolean') {
|
|
78
|
+
return {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
description: definition.description
|
|
81
|
+
}
|
|
82
|
+
} else if(type === 'Date') {
|
|
83
|
+
return {
|
|
84
|
+
type: 'string',
|
|
85
|
+
format: 'date-time',
|
|
86
|
+
description: definition.description
|
|
87
|
+
}
|
|
88
|
+
} else if(type) {
|
|
89
|
+
const api = useApi(appContext)
|
|
90
|
+
const [serviceName, modelName] = definition.type.split('_')
|
|
91
|
+
const serviceDefinition = api.getServiceDefinition(serviceName)
|
|
92
|
+
const modelDefinition = serviceDefinition?.models?.[modelName]
|
|
93
|
+
if(!data || typeof data === 'string') {
|
|
94
|
+
return {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: `Id of ${modelName} from ${serviceName} service.`
|
|
97
|
+
+ (modelDefinition?.description ? `\n${modelDefinition.description}` : '')
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const schema = schemaFromDefinition(modelDefinition, data, 'Object', appContext)
|
|
101
|
+
schema.serviceName = serviceName
|
|
102
|
+
schema.modelName = modelName
|
|
103
|
+
schema.description = [
|
|
104
|
+
`Object ${modelName} from ${serviceName} service.`,
|
|
105
|
+
definition.description,schema.description
|
|
106
|
+
].filter(Boolean).join('\n')
|
|
107
|
+
return schema
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
console.log("UNHANDLED TYPE", definition)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extendSchema(schema, data, viewName, serviceName, appContext) {
|
|
115
|
+
if(!data) return
|
|
116
|
+
if(Array.isArray(data)) {
|
|
117
|
+
if(!schema.items) {
|
|
118
|
+
schema.items = generateSchema(data, schema, 'items', appContext)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
for(const property in data) {
|
|
122
|
+
const value = data[property]
|
|
123
|
+
if(!schema.properties[property]) { /// additional property
|
|
124
|
+
if(property === 'id' || property === 'to') {
|
|
125
|
+
schema.properties.id = {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: schema.modelName ? `Id of ${schema.modelName} from ${schema.serviceName} service.` : `Id.`
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
generateSchema(value, schema.properties, property, appContext)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function generateSchema(data, schemaObject, schemaProperty, appContext) {
|
|
138
|
+
const api = useApi(appContext)
|
|
139
|
+
if(!schemaObject[schemaProperty]) {
|
|
140
|
+
schemaObject[schemaProperty] = {
|
|
141
|
+
anyOf: []
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let schemas = schemaObject[schemaProperty].anyOf
|
|
145
|
+
let viewDefinition = null
|
|
146
|
+
if(data?.[sourceSymbol]) {
|
|
147
|
+
const [serviceName, viewName] = data[sourceSymbol]
|
|
148
|
+
const serviceDefinition = api.getServiceDefinition(serviceName)
|
|
149
|
+
viewDefinition = serviceDefinition?.views?.[viewName]
|
|
150
|
+
}
|
|
151
|
+
if(viewDefinition) {
|
|
152
|
+
//console.log("VIEW DEFINITION", viewDefinition)
|
|
153
|
+
const schema = schemaFromDefinition(viewDefinition.returns, data, undefined, appContext)
|
|
154
|
+
extendSchema(schema, data, undefined, undefined, appContext)
|
|
155
|
+
addSchema(schemas, schema)
|
|
156
|
+
} else {
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cleanSchema(schema) {
|
|
162
|
+
if(schema.anyOf && schema.anyOf.length === 1) {
|
|
163
|
+
return cleanSchema(schema.anyOf[0])
|
|
164
|
+
} else if(schema.type === 'object') {
|
|
165
|
+
const cleanedProperties = Object.fromEntries(
|
|
166
|
+
Object.entries(schema.properties).map(([key, value]) => [key, cleanSchema(value)])
|
|
167
|
+
)
|
|
168
|
+
return {
|
|
169
|
+
...schema,
|
|
170
|
+
properties: cleanedProperties
|
|
171
|
+
}
|
|
172
|
+
} else if(schema.type === 'array') {
|
|
173
|
+
return {
|
|
174
|
+
type: 'array',
|
|
175
|
+
items: cleanSchema(schema.items)
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
return schema
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getSchemaFromData(data, appContext = getCurrentInstance().appContext) {
|
|
183
|
+
let schemaOutput = {}
|
|
184
|
+
generateSchema(data, schemaOutput, 'schema', appContext)
|
|
185
|
+
return cleanSchema(schemaOutput.schema)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function cleanData(data) {
|
|
189
|
+
if(typeof data !== 'object') return data
|
|
190
|
+
if(Array.isArray(data)) {
|
|
191
|
+
return data.map(cleanData)
|
|
192
|
+
} else {
|
|
193
|
+
const cleanedProperties = Object.fromEntries(
|
|
194
|
+
Object.entries(data).map(([key, value]) => [key, cleanData(value)])
|
|
195
|
+
)
|
|
196
|
+
if(data.to && data.id) {
|
|
197
|
+
cleanedProperties.id = data.to
|
|
198
|
+
delete cleanedProperties.to
|
|
199
|
+
}
|
|
200
|
+
return cleanedProperties
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="w-full lg:w-8/12 md:w-11/12">
|
|
3
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
4
|
|
|
11
5
|
<ActionForm
|
|
12
6
|
:service="serviceName"
|
|
13
|
-
:action="actionName"
|
|
7
|
+
:action="actionName"
|
|
8
|
+
:parameters="parameters"
|
|
14
9
|
@done="handleDone"
|
|
15
10
|
/>
|
|
11
|
+
|
|
16
12
|
</div>
|
|
17
13
|
</div>
|
|
18
14
|
</template>
|
|
@@ -29,9 +25,14 @@
|
|
|
29
25
|
actionName: {
|
|
30
26
|
type: String,
|
|
31
27
|
required: true
|
|
28
|
+
},
|
|
29
|
+
parametersJson: {
|
|
30
|
+
type: String
|
|
32
31
|
}
|
|
33
32
|
})
|
|
34
|
-
const { serviceName, actionName } = toRefs(props)
|
|
33
|
+
const { serviceName, actionName, parametersJson } = toRefs(props)
|
|
34
|
+
|
|
35
|
+
const parameters = computed(() => parametersJson.value ? JSON.parse(parametersJson.value) : {})
|
|
35
36
|
|
|
36
37
|
import { useApi } from '@live-change/vue3-ssr'
|
|
37
38
|
const api = useApi()
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<div class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border">
|
|
9
9
|
|
|
10
10
|
<ModelEditor :service="serviceName" :model="modelName" :identifiers="identifiersObject" draft
|
|
11
|
-
@created="handleCreated"/>
|
|
11
|
+
@created="handleCreated" @saved="handleSaved" />
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
//console.log("newIdentifiers", newIdentifiers)
|
|
84
84
|
if(JSON.stringify(identifiers.value) !== JSON.stringify(newIdentifiers)) {
|
|
85
85
|
router.push({
|
|
86
|
-
name: 'auto-form:
|
|
86
|
+
name: 'auto-form:view',
|
|
87
87
|
params: {
|
|
88
88
|
serviceName: serviceName.value,
|
|
89
89
|
modelName: modelName.value,
|
|
@@ -93,6 +93,17 @@
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function handleSaved(id) {
|
|
97
|
+
router.push({
|
|
98
|
+
name: 'auto-form:view',
|
|
99
|
+
params: {
|
|
100
|
+
serviceName: serviceName.value,
|
|
101
|
+
modelName: modelName.value,
|
|
102
|
+
identifiers: identifiers.value
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
</script>
|
|
97
108
|
|
|
98
109
|
<style scoped>
|
package/front/src/router.js
CHANGED
|
@@ -38,6 +38,12 @@ export function autoFormRoutes(config = {}) {
|
|
|
38
38
|
props: true
|
|
39
39
|
}),
|
|
40
40
|
|
|
41
|
+
route({
|
|
42
|
+
name: 'auto-form:actionParameters', path: prefix + '/action/:serviceName/:actionName/:parametersJson', meta: { },
|
|
43
|
+
component: () => import("./pages/Action.vue"),
|
|
44
|
+
props: true
|
|
45
|
+
}),
|
|
46
|
+
|
|
41
47
|
]
|
|
42
48
|
}
|
|
43
49
|
|
package/index.js
CHANGED
|
@@ -45,6 +45,12 @@ export { AutoObjectIdentification }
|
|
|
45
45
|
|
|
46
46
|
export * from './front/src/router.js'
|
|
47
47
|
|
|
48
|
+
import DataWithSchema from './front/src/components/schema/DataWithSchema.vue'
|
|
49
|
+
export { DataWithSchema }
|
|
50
|
+
import SchemaFromDefinition from './front/src/components/schema/SchemaFromDefinition.vue'
|
|
51
|
+
export { SchemaFromDefinition }
|
|
52
|
+
export * from './front/src/logic/schema.js'
|
|
53
|
+
|
|
48
54
|
import en from "./front/locales/en.json"
|
|
49
55
|
const locales = { en }
|
|
50
56
|
export { locales }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/frontend-auto-form",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.86",
|
|
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.
|
|
26
|
-
"@live-change/dao": "^0.9.
|
|
27
|
-
"@live-change/dao-vue3": "^0.9.
|
|
28
|
-
"@live-change/dao-websocket": "^0.9.
|
|
29
|
-
"@live-change/framework": "^0.9.
|
|
30
|
-
"@live-change/image-frontend": "^0.9.
|
|
31
|
-
"@live-change/image-service": "^0.9.
|
|
32
|
-
"@live-change/session-service": "^0.9.
|
|
33
|
-
"@live-change/vue3-components": "^0.9.
|
|
34
|
-
"@live-change/vue3-ssr": "^0.9.
|
|
25
|
+
"@live-change/cli": "^0.9.86",
|
|
26
|
+
"@live-change/dao": "^0.9.86",
|
|
27
|
+
"@live-change/dao-vue3": "^0.9.86",
|
|
28
|
+
"@live-change/dao-websocket": "^0.9.86",
|
|
29
|
+
"@live-change/framework": "^0.9.86",
|
|
30
|
+
"@live-change/image-frontend": "^0.9.86",
|
|
31
|
+
"@live-change/image-service": "^0.9.86",
|
|
32
|
+
"@live-change/session-service": "^0.9.86",
|
|
33
|
+
"@live-change/vue3-components": "^0.9.86",
|
|
34
|
+
"@live-change/vue3-ssr": "^0.9.86",
|
|
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.
|
|
55
|
+
"@live-change/codeceptjs-helper": "^0.9.86",
|
|
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": "
|
|
66
|
+
"gitHead": "7b0013ef101d9033402eeec45fd1be60e151aada"
|
|
67
67
|
}
|