@live-change/frontend-auto-form 0.9.85 → 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/ActionButtons.vue +80 -0
- package/front/src/components/crud/ActionForm.vue +137 -8
- package/front/src/components/crud/ModelEditor.vue +23 -6
- package/front/src/components/crud/ModelView.vue +72 -19
- package/front/src/components/schema/DataWithSchema.vue +15 -11
- package/front/src/components/schema/SchemaFromDefinition.vue +23 -0
- package/front/src/logic/actionData.js +8 -5
- package/front/src/logic/schema.js +31 -12
- 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 +2 -0
- package/package.json +13 -13
|
@@ -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
|
-
<
|
|
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,10 +26,17 @@
|
|
|
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">
|
|
@@ -162,10 +169,20 @@
|
|
|
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)
|
|
165
|
-
}
|
|
166
|
-
|
|
172
|
+
} else {
|
|
173
|
+
emit('saved', saveResult)
|
|
174
|
+
}
|
|
167
175
|
}
|
|
168
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
|
+
|
|
169
186
|
const scopesPath = computed(() => path.scope.objectScopes({
|
|
170
187
|
objectType: parentObjects.value[0].objectType, /// TODO: support multiple parent objects!
|
|
171
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
|
|
|
@@ -41,6 +32,15 @@
|
|
|
41
32
|
|
|
42
33
|
</div>
|
|
43
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
|
+
|
|
44
44
|
<div v-for="preparedRelation of visibleObjectRelations" class="mb-6">
|
|
45
45
|
<ModelSingle :service="preparedRelation.service" :model="preparedRelation.model"
|
|
46
46
|
:views="preparedRelation.views">
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
<div class="bg-surface-0 dark:bg-surface-900 p-4 shadow-sm rounded-border">
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
<!-- <pre>visibleRangeRelations = {{ visibleRangeRelations }}</pre>
|
|
102
102
|
<pre>preparedRelations = {{ preparedRelations }}</pre>
|
|
103
103
|
|
|
104
104
|
<div v-if="backwardRelations">
|
|
@@ -109,9 +109,13 @@
|
|
|
109
109
|
)
|
|
110
110
|
}}</pre>
|
|
111
111
|
</div>
|
|
112
|
+
<pre>accessControlRoles = {{ accessControlRoles }}</pre> -->
|
|
113
|
+
|
|
114
|
+
<pre>identifiers = {{ identifiers }}</pre>
|
|
112
115
|
|
|
116
|
+
<pre>definition = {{ modelDefinition }}</pre>
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
<pre>object = {{ object }}</pre>
|
|
115
119
|
|
|
116
120
|
</div>
|
|
117
121
|
|
|
@@ -178,6 +182,24 @@
|
|
|
178
182
|
return api.services?.[service.value]?.models?.[model.value]
|
|
179
183
|
})
|
|
180
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
|
+
|
|
181
203
|
import { getForwardRelations, getBackwardRelations, anyRelationsTypes, prepareObjectRelations }
|
|
182
204
|
from '../../logic/relations.js'
|
|
183
205
|
const forwardRelations = computed(() => getForwardRelations(modelDefinition.value, () => true, api))
|
|
@@ -205,14 +227,18 @@
|
|
|
205
227
|
return prepareObjectRelations(objectType.value, object.value.to ?? object.value.id, api)
|
|
206
228
|
})
|
|
207
229
|
|
|
208
|
-
const visibleRangeRelations = computed(() => preparedRelations.value
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
)
|
|
216
242
|
|
|
217
243
|
const visibleObjectRelations = computed(() => preparedRelations.value.filter(preparedRelation => {
|
|
218
244
|
if(!preparedRelation.singular) return false
|
|
@@ -236,6 +262,33 @@
|
|
|
236
262
|
}
|
|
237
263
|
}))
|
|
238
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
|
+
}
|
|
239
292
|
|
|
240
293
|
</script>
|
|
241
294
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup>
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
data: {
|
|
20
20
|
type: Object,
|
|
21
21
|
required: true,
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
|
+
prefix: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: ''
|
|
26
|
+
}
|
|
23
27
|
})
|
|
24
28
|
const { data } = toRefs(props)
|
|
25
29
|
|
|
@@ -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,
|
|
@@ -28,21 +28,40 @@ function addSchema(schemas, schema) {
|
|
|
28
28
|
export function schemaFromDefinition(definition, data, type, appContext = getCurrentInstance().appContext) {
|
|
29
29
|
if(!type) type = definition.type
|
|
30
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
|
+
}
|
|
31
50
|
return {
|
|
32
51
|
type: 'object',
|
|
33
|
-
properties
|
|
34
|
-
Object.entries(definition.properties).map(([key, value]) => [key, schemaFromDefinition(value, data[key], undefined, appContext)])
|
|
35
|
-
),
|
|
52
|
+
properties,
|
|
36
53
|
description: definition.description
|
|
37
54
|
}
|
|
38
55
|
} else if(type === 'Array') {
|
|
39
56
|
const schema = {
|
|
40
57
|
type: 'array',
|
|
41
|
-
items: schemaFromDefinition(definition.items ?? definition.of, data[0], undefined, appContext),
|
|
58
|
+
items: schemaFromDefinition(definition.items ?? definition.of, data?.[0], undefined, appContext),
|
|
42
59
|
description: definition.description
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
if(data) {
|
|
62
|
+
for(const item of data) {
|
|
63
|
+
extendSchema(schema.items, item, schema.items.modelName, schema.items.serviceName)
|
|
64
|
+
}
|
|
46
65
|
}
|
|
47
66
|
return schema
|
|
48
67
|
} else if(type === 'String') {
|
|
@@ -70,9 +89,8 @@ export function schemaFromDefinition(definition, data, type, appContext = getCur
|
|
|
70
89
|
const api = useApi(appContext)
|
|
71
90
|
const [serviceName, modelName] = definition.type.split('_')
|
|
72
91
|
const serviceDefinition = api.getServiceDefinition(serviceName)
|
|
73
|
-
const modelDefinition = serviceDefinition?.models?.[modelName]
|
|
74
|
-
|
|
75
|
-
if(typeof data === 'string') {
|
|
92
|
+
const modelDefinition = serviceDefinition?.models?.[modelName]
|
|
93
|
+
if(!data || typeof data === 'string') {
|
|
76
94
|
return {
|
|
77
95
|
type: 'string',
|
|
78
96
|
description: `Id of ${modelName} from ${serviceName} service.`
|
|
@@ -94,6 +112,7 @@ export function schemaFromDefinition(definition, data, type, appContext = getCur
|
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
function extendSchema(schema, data, viewName, serviceName, appContext) {
|
|
115
|
+
if(!data) return
|
|
97
116
|
if(Array.isArray(data)) {
|
|
98
117
|
if(!schema.items) {
|
|
99
118
|
schema.items = generateSchema(data, schema, 'items', appContext)
|
|
@@ -108,7 +127,7 @@ function extendSchema(schema, data, viewName, serviceName, appContext) {
|
|
|
108
127
|
description: schema.modelName ? `Id of ${schema.modelName} from ${schema.serviceName} service.` : `Id.`
|
|
109
128
|
}
|
|
110
129
|
} else {
|
|
111
|
-
generateSchema(value, schema.properties, property)
|
|
130
|
+
generateSchema(value, schema.properties, property, appContext)
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
}
|
|
@@ -116,7 +135,7 @@ function extendSchema(schema, data, viewName, serviceName, appContext) {
|
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
function generateSchema(data, schemaObject, schemaProperty, appContext) {
|
|
119
|
-
const api = useApi(appContext)
|
|
138
|
+
const api = useApi(appContext)
|
|
120
139
|
if(!schemaObject[schemaProperty]) {
|
|
121
140
|
schemaObject[schemaProperty] = {
|
|
122
141
|
anyOf: []
|
|
@@ -180,4 +199,4 @@ export function cleanData(data) {
|
|
|
180
199
|
}
|
|
181
200
|
return cleanedProperties
|
|
182
201
|
}
|
|
183
|
-
}
|
|
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
|
@@ -47,6 +47,8 @@ export * from './front/src/router.js'
|
|
|
47
47
|
|
|
48
48
|
import DataWithSchema from './front/src/components/schema/DataWithSchema.vue'
|
|
49
49
|
export { DataWithSchema }
|
|
50
|
+
import SchemaFromDefinition from './front/src/components/schema/SchemaFromDefinition.vue'
|
|
51
|
+
export { SchemaFromDefinition }
|
|
50
52
|
export * from './front/src/logic/schema.js'
|
|
51
53
|
|
|
52
54
|
import en from "./front/locales/en.json"
|
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
|
}
|