@live-change/frontend-auto-form 0.9.83 → 0.9.85
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/ModelEditor.vue +7 -6
- package/front/src/components/crud/ModelView.vue +16 -1
- package/front/src/components/schema/DataWithSchema.vue +37 -0
- package/front/src/logic/editorData.js +4 -4
- package/front/src/logic/relations.js +3 -0
- package/front/src/logic/schema.js +183 -0
- package/index.js +4 -0
- package/package.json +13 -13
- package/front/src/components/crud/ActionButtons.vue +0 -80
|
@@ -33,15 +33,16 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<form v-if="editor" @submit="handleSave" @reset="handleReset">
|
|
36
|
-
<div v-for="identifier in modelDefinition.identifiers">
|
|
36
|
+
<div v-for="identifier in modelDefinition.identifiers">
|
|
37
37
|
<template v-if="(identifier.name ?? identifier).slice(-4) !== 'Type'">
|
|
38
|
-
<div v-if="identifiers[identifier]" class="flex flex-col mb-3">
|
|
38
|
+
<div v-if="identifiers[identifier.name ?? identifier]" class="flex flex-col mb-3">
|
|
39
39
|
<div class="min-w-[8rem] font-medium">{{ identifier.name ?? identifier }}</div>
|
|
40
40
|
<div class="">
|
|
41
|
-
<InjectedObjectIndentification
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
<InjectedObjectIndentification v-if="identifiers[(identifier.name ?? identifier)+'Type']
|
|
42
|
+
?? modelDefinition.properties[identifier.field ?? identifier]?.type"
|
|
43
|
+
:type="identifiers[(identifier.name ?? identifier)+'Type']
|
|
44
|
+
?? modelDefinition.properties[identifier.field ?? identifier]?.type"
|
|
45
|
+
:object="identifiers[identifier.name ?? identifier]"
|
|
45
46
|
/>
|
|
46
47
|
</div>
|
|
47
48
|
</div>
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
class="ml-2"
|
|
29
29
|
/>
|
|
30
30
|
</div>
|
|
31
|
-
<
|
|
31
|
+
<div class="flex flex-row flex-wrap justify-between align-items-top gap-2">
|
|
32
|
+
<Button label="Access" icon="pi pi-key" class="p-button mb-6" @click="showAccessControl" />
|
|
33
|
+
<router-link :to="editRoute">
|
|
34
|
+
<Button label="Edit" icon="pi pi-pencil" class="p-button mb-6" />
|
|
35
|
+
</router-link>
|
|
36
|
+
</div>
|
|
32
37
|
</div>
|
|
33
38
|
|
|
34
39
|
<AutoView :value="object" :root-value="object" :i18n="i18n" :attributes="attributes"
|
|
@@ -222,6 +227,16 @@
|
|
|
222
227
|
}
|
|
223
228
|
const accessControlRoles = computed(() => modelDefinition.value?.accessRoles ?? [])
|
|
224
229
|
|
|
230
|
+
const editRoute = computed(() => ({
|
|
231
|
+
name: 'auto-form:editor',
|
|
232
|
+
params: {
|
|
233
|
+
serviceName: service.value,
|
|
234
|
+
modelName: model.value,
|
|
235
|
+
identifiers: Object.values(identifiers.value)
|
|
236
|
+
}
|
|
237
|
+
}))
|
|
238
|
+
|
|
239
|
+
|
|
225
240
|
</script>
|
|
226
241
|
|
|
227
242
|
<style scoped>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="surface-card p-3">
|
|
4
|
+
<h4>#### Schema:</h4>
|
|
5
|
+
<pre>{{ '```\n' + JSON.stringify(schema, null, 2) + '\n```' }}</pre>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="surface-card p-3">
|
|
8
|
+
<h4>#### Data:</h4>
|
|
9
|
+
<pre>{{ '```\n' + JSON.stringify(clearData, null, 2) + '\n```' }}</pre>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
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
|
+
})
|
|
24
|
+
const { data } = toRefs(props)
|
|
25
|
+
|
|
26
|
+
import { getSchemaFromData, cleanData } from "../../logic/schema.js"
|
|
27
|
+
|
|
28
|
+
const appContext = getCurrentInstance().appContext
|
|
29
|
+
|
|
30
|
+
const schema = computed(() => getSchemaFromData(data.value, appContext))
|
|
31
|
+
const clearData = computed(() => cleanData(data.value))
|
|
32
|
+
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style scoped>
|
|
36
|
+
|
|
37
|
+
</style>
|
|
@@ -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,183 @@
|
|
|
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
|
+
return {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: Object.fromEntries(
|
|
34
|
+
Object.entries(definition.properties).map(([key, value]) => [key, schemaFromDefinition(value, data[key], undefined, appContext)])
|
|
35
|
+
),
|
|
36
|
+
description: definition.description
|
|
37
|
+
}
|
|
38
|
+
} else if(type === 'Array') {
|
|
39
|
+
const schema = {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: schemaFromDefinition(definition.items ?? definition.of, data[0], undefined, appContext),
|
|
42
|
+
description: definition.description
|
|
43
|
+
}
|
|
44
|
+
for(const item of data) {
|
|
45
|
+
extendSchema(schema.items, item, schema.items.modelName, schema.items.serviceName)
|
|
46
|
+
}
|
|
47
|
+
return schema
|
|
48
|
+
} else if(type === 'String') {
|
|
49
|
+
return {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: definition.description
|
|
52
|
+
}
|
|
53
|
+
} else if(type === 'Number') {
|
|
54
|
+
return {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: definition.description
|
|
57
|
+
}
|
|
58
|
+
} else if(type === 'Boolean') {
|
|
59
|
+
return {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description: definition.description
|
|
62
|
+
}
|
|
63
|
+
} else if(type === 'Date') {
|
|
64
|
+
return {
|
|
65
|
+
type: 'string',
|
|
66
|
+
format: 'date-time',
|
|
67
|
+
description: definition.description
|
|
68
|
+
}
|
|
69
|
+
} else if(type) {
|
|
70
|
+
const api = useApi(appContext)
|
|
71
|
+
const [serviceName, modelName] = definition.type.split('_')
|
|
72
|
+
const serviceDefinition = api.getServiceDefinition(serviceName)
|
|
73
|
+
const modelDefinition = serviceDefinition?.models?.[modelName]
|
|
74
|
+
//console.log("MODEL DEFINITION", modelDefinition, "DATA", data, typeof data)
|
|
75
|
+
if(typeof data === 'string') {
|
|
76
|
+
return {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: `Id of ${modelName} from ${serviceName} service.`
|
|
79
|
+
+ (modelDefinition?.description ? `\n${modelDefinition.description}` : '')
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
const schema = schemaFromDefinition(modelDefinition, data, 'Object', appContext)
|
|
83
|
+
schema.serviceName = serviceName
|
|
84
|
+
schema.modelName = modelName
|
|
85
|
+
schema.description = [
|
|
86
|
+
`Object ${modelName} from ${serviceName} service.`,
|
|
87
|
+
definition.description,schema.description
|
|
88
|
+
].filter(Boolean).join('\n')
|
|
89
|
+
return schema
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log("UNHANDLED TYPE", definition)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extendSchema(schema, data, viewName, serviceName, appContext) {
|
|
97
|
+
if(Array.isArray(data)) {
|
|
98
|
+
if(!schema.items) {
|
|
99
|
+
schema.items = generateSchema(data, schema, 'items', appContext)
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
for(const property in data) {
|
|
103
|
+
const value = data[property]
|
|
104
|
+
if(!schema.properties[property]) { /// additional property
|
|
105
|
+
if(property === 'id' || property === 'to') {
|
|
106
|
+
schema.properties.id = {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: schema.modelName ? `Id of ${schema.modelName} from ${schema.serviceName} service.` : `Id.`
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
generateSchema(value, schema.properties, property)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateSchema(data, schemaObject, schemaProperty, appContext) {
|
|
119
|
+
const api = useApi(appContext)
|
|
120
|
+
if(!schemaObject[schemaProperty]) {
|
|
121
|
+
schemaObject[schemaProperty] = {
|
|
122
|
+
anyOf: []
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let schemas = schemaObject[schemaProperty].anyOf
|
|
126
|
+
let viewDefinition = null
|
|
127
|
+
if(data?.[sourceSymbol]) {
|
|
128
|
+
const [serviceName, viewName] = data[sourceSymbol]
|
|
129
|
+
const serviceDefinition = api.getServiceDefinition(serviceName)
|
|
130
|
+
viewDefinition = serviceDefinition?.views?.[viewName]
|
|
131
|
+
}
|
|
132
|
+
if(viewDefinition) {
|
|
133
|
+
//console.log("VIEW DEFINITION", viewDefinition)
|
|
134
|
+
const schema = schemaFromDefinition(viewDefinition.returns, data, undefined, appContext)
|
|
135
|
+
extendSchema(schema, data, undefined, undefined, appContext)
|
|
136
|
+
addSchema(schemas, schema)
|
|
137
|
+
} else {
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cleanSchema(schema) {
|
|
143
|
+
if(schema.anyOf && schema.anyOf.length === 1) {
|
|
144
|
+
return cleanSchema(schema.anyOf[0])
|
|
145
|
+
} else if(schema.type === 'object') {
|
|
146
|
+
const cleanedProperties = Object.fromEntries(
|
|
147
|
+
Object.entries(schema.properties).map(([key, value]) => [key, cleanSchema(value)])
|
|
148
|
+
)
|
|
149
|
+
return {
|
|
150
|
+
...schema,
|
|
151
|
+
properties: cleanedProperties
|
|
152
|
+
}
|
|
153
|
+
} else if(schema.type === 'array') {
|
|
154
|
+
return {
|
|
155
|
+
type: 'array',
|
|
156
|
+
items: cleanSchema(schema.items)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
return schema
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getSchemaFromData(data, appContext = getCurrentInstance().appContext) {
|
|
164
|
+
let schemaOutput = {}
|
|
165
|
+
generateSchema(data, schemaOutput, 'schema', appContext)
|
|
166
|
+
return cleanSchema(schemaOutput.schema)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function cleanData(data) {
|
|
170
|
+
if(typeof data !== 'object') return data
|
|
171
|
+
if(Array.isArray(data)) {
|
|
172
|
+
return data.map(cleanData)
|
|
173
|
+
} else {
|
|
174
|
+
const cleanedProperties = Object.fromEntries(
|
|
175
|
+
Object.entries(data).map(([key, value]) => [key, cleanData(value)])
|
|
176
|
+
)
|
|
177
|
+
if(data.to && data.id) {
|
|
178
|
+
cleanedProperties.id = data.to
|
|
179
|
+
delete cleanedProperties.to
|
|
180
|
+
}
|
|
181
|
+
return cleanedProperties
|
|
182
|
+
}
|
|
183
|
+
}
|
package/index.js
CHANGED
|
@@ -45,6 +45,10 @@ 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
|
+
export * from './front/src/logic/schema.js'
|
|
51
|
+
|
|
48
52
|
import en from "./front/locales/en.json"
|
|
49
53
|
const locales = { en }
|
|
50
54
|
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.85",
|
|
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.85",
|
|
26
|
+
"@live-change/dao": "^0.9.85",
|
|
27
|
+
"@live-change/dao-vue3": "^0.9.85",
|
|
28
|
+
"@live-change/dao-websocket": "^0.9.85",
|
|
29
|
+
"@live-change/framework": "^0.9.85",
|
|
30
|
+
"@live-change/image-frontend": "^0.9.85",
|
|
31
|
+
"@live-change/image-service": "^0.9.85",
|
|
32
|
+
"@live-change/session-service": "^0.9.85",
|
|
33
|
+
"@live-change/vue3-components": "^0.9.85",
|
|
34
|
+
"@live-change/vue3-ssr": "^0.9.85",
|
|
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.85",
|
|
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": "126afb0aad3ab6e03aa5742726f429c95c46783a"
|
|
67
67
|
}
|
|
@@ -1,80 +0,0 @@
|
|
|
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>
|