@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.
@@ -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
- :type="identifiers[(identifier.field ?? identifier)+'Type']
43
- ?? modelDefinition.properties[identifier.field ?? identifier].type"
44
- :object="identifiers[identifier.field ?? identifier]"
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
- <Button label="Access" icon="pi pi-key" class="p-button mb-6" @click="showAccessControl" />
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.83",
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.83",
26
- "@live-change/dao": "^0.9.83",
27
- "@live-change/dao-vue3": "^0.9.83",
28
- "@live-change/dao-websocket": "^0.9.83",
29
- "@live-change/framework": "^0.9.83",
30
- "@live-change/image-frontend": "^0.9.83",
31
- "@live-change/image-service": "^0.9.83",
32
- "@live-change/session-service": "^0.9.83",
33
- "@live-change/vue3-components": "^0.9.83",
34
- "@live-change/vue3-ssr": "^0.9.83",
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.83",
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": "db3fcd19e6e3a47dda84d3906e697623f057713a"
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>