parent
b24a8b2805
commit
95381c1da3
@ -0,0 +1,161 @@ |
|||||||
|
<script> |
||||||
|
import {castId, keyBy, objCopy, isEmpty} from "../utils"; |
||||||
|
import forEach from "lodash-es/forEach"; |
||||||
|
import axios from "axios"; |
||||||
|
|
||||||
|
export default { |
||||||
|
props: ['object', 'schema', 'objects'], |
||||||
|
name: "EditObjectForm", |
||||||
|
data() { |
||||||
|
let object = this.object; |
||||||
|
|
||||||
|
const model = this.schema.obj_models.find((m) => m.id === object.model); |
||||||
|
let properties = this.schema.prop_models.filter((m) => m.object === model.id); |
||||||
|
let relations = this.schema.rel_models.filter((m) => m.object === model.id); |
||||||
|
|
||||||
|
properties.sort((a, b) => a.name.localeCompare(b.name)); |
||||||
|
relations.sort((a, b) => a.name.localeCompare(b.name)); |
||||||
|
|
||||||
|
let values = {}; |
||||||
|
properties.forEach((p) => { |
||||||
|
let existing = object.values[p.id] || []; |
||||||
|
|
||||||
|
if (existing.length) { |
||||||
|
values[p.id] = existing; |
||||||
|
} else { |
||||||
|
if (p.optional) { |
||||||
|
values[p.id] = []; |
||||||
|
} else { |
||||||
|
values[p.id] = [ |
||||||
|
// this is the format used for values |
||||||
|
{ |
||||||
|
id: null, |
||||||
|
// it can also have model: ... here |
||||||
|
value: objCopy(p.default) |
||||||
|
} |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
properties = keyBy(properties, 'id'); |
||||||
|
relations = keyBy(relations, 'id'); |
||||||
|
|
||||||
|
let model_names = {}; |
||||||
|
this.schema.obj_models.forEach((m) => { |
||||||
|
model_names[m.id] = m.name; |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
model, |
||||||
|
properties, |
||||||
|
relations, |
||||||
|
haveRelations: !isEmpty(relations), |
||||||
|
model_names, |
||||||
|
values, |
||||||
|
name: object.name, |
||||||
|
relationRefs: [], |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** Get values in the raw format without grouping */ |
||||||
|
collectData() { |
||||||
|
if (isEmpty(this.name)) { |
||||||
|
throw new Error("Name is required"); |
||||||
|
} |
||||||
|
|
||||||
|
let values = []; |
||||||
|
forEach(objCopy(this.values), (vv, prop_model_id) => { |
||||||
|
for (let v of vv) { |
||||||
|
v.model_id = castId(prop_model_id); |
||||||
|
values.push(v); |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let relations = []; |
||||||
|
for (let rref of this.relationRefs) { |
||||||
|
for (let r of rref.collectData()) { |
||||||
|
relations.push(r); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
model_id: this.object.model, // string is fine |
||||||
|
id: this.object.id, |
||||||
|
name: this.name, |
||||||
|
values, |
||||||
|
relations, |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
trySave() { |
||||||
|
let data; |
||||||
|
try { |
||||||
|
data = this.collectData(); |
||||||
|
} catch (e) { |
||||||
|
alert(e.message); |
||||||
|
return; |
||||||
|
} |
||||||
|
console.log('Try save', data); |
||||||
|
|
||||||
|
axios({ |
||||||
|
method: 'post', |
||||||
|
url: '/object/update', |
||||||
|
data: data |
||||||
|
}) |
||||||
|
.then(function (response) { |
||||||
|
location.href = '/object/detail/'+this.object.id; |
||||||
|
}) |
||||||
|
.catch(function (error) { |
||||||
|
// TODO show error toast instead |
||||||
|
alert(error.response ? |
||||||
|
error.response.data : |
||||||
|
error) |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setRelationRef(el) { |
||||||
|
if (el) { |
||||||
|
this.relationRefs.push(el) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
beforeUpdate() { |
||||||
|
this.relationRefs = [] |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$el.parentNode |
||||||
|
.classList.add('EditForm'); |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<h2>Edit {{ model.name }}</h2> |
||||||
|
|
||||||
|
<p><input type="button" value="Save" @click="trySave"></p> |
||||||
|
|
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<th><label for="field-name">Name</label></th> |
||||||
|
<td> |
||||||
|
<input type="text" id="field-name" v-model="name"> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property> |
||||||
|
</table> |
||||||
|
|
||||||
|
<div v-if="haveRelations"> |
||||||
|
<h3>Relations</h3> |
||||||
|
|
||||||
|
<edit-relation |
||||||
|
v-for="relation in relations" |
||||||
|
:ref="setRelationRef" |
||||||
|
:model_id="relation.id" |
||||||
|
:objects="objects" |
||||||
|
:initialInstances="object.relations[relation.id]" |
||||||
|
:schema="schema" |
||||||
|
></edit-relation> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,62 @@ |
|||||||
|
<script> |
||||||
|
import {objCopy, uniqueId} from "../utils"; |
||||||
|
import * as Vue from 'vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "EditPropertyField", |
||||||
|
props: ['model', 'values'], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
id: uniqueId(), |
||||||
|
fieldRefs: [], |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
addValue(event) { |
||||||
|
this.values.push({ |
||||||
|
id: null, |
||||||
|
value: objCopy(this.model.default) |
||||||
|
}); |
||||||
|
|
||||||
|
Vue.nextTick(() => { |
||||||
|
this.fieldRefs[this.values.length-1].focus(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
removeValue(vi) { |
||||||
|
this.values.splice(vi, 1); |
||||||
|
}, |
||||||
|
setFieldRef(el) { |
||||||
|
if (el) { |
||||||
|
this.fieldRefs.push(el) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
beforeUpdate() { |
||||||
|
this.fieldRefs = [] |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<tr v-if="values.length===0"> |
||||||
|
<th @click="addValue">{{model.name}}</th> |
||||||
|
<td> |
||||||
|
<a href="#" @click="addValue">Add</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr v-else v-for="(instance, vi) in values" :key="vi"> |
||||||
|
<th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="id">{{model.name}}</label></th> |
||||||
|
<td> |
||||||
|
<string-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='String'"></string-value> |
||||||
|
<integer-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Integer'"></integer-value> |
||||||
|
<decimal-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Decimal'"></decimal-value> |
||||||
|
<boolean-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Boolean'"></boolean-value> |
||||||
|
<a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr v-if="values.length > 0 && model.multiple"> |
||||||
|
<td> |
||||||
|
<a href="#" @click="addValue">Add</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</template> |
@ -0,0 +1,121 @@ |
|||||||
|
<script> |
||||||
|
import {castId, isEmpty, keyBy, objCopy} from "../utils"; |
||||||
|
import forEach from "lodash-es/forEach"; |
||||||
|
|
||||||
|
export default { |
||||||
|
props: ['model_id', 'schema', 'objects', 'initialInstances'], |
||||||
|
name: "EditRelationForm", |
||||||
|
data() { |
||||||
|
const model = this.schema.rel_models.find((m) => m.id === this.model_id); |
||||||
|
if(!model) throw Error("Relation model not exist"); |
||||||
|
|
||||||
|
let properties = this.schema.prop_models.filter((m) => m.object === model.id); |
||||||
|
|
||||||
|
properties.sort((a, b) => a.name.localeCompare(b.name)); |
||||||
|
|
||||||
|
if (isEmpty(properties)) { |
||||||
|
properties = null; |
||||||
|
} else { |
||||||
|
properties = keyBy(properties, 'id'); |
||||||
|
} |
||||||
|
|
||||||
|
let related_model = this.schema.obj_models.find((m) => m.id === model.related); |
||||||
|
|
||||||
|
if(!related_model) throw Error("Related model not exist"); |
||||||
|
|
||||||
|
let choices = {}; |
||||||
|
this.objects.forEach((obj) => { |
||||||
|
if (obj.model === model.related) { |
||||||
|
choices[obj.id] = obj.name; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
model, |
||||||
|
related_model, |
||||||
|
properties, |
||||||
|
object_names: choices, |
||||||
|
instances: objCopy(this.initialInstances), |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
collectData() { |
||||||
|
console.log('relation->collect', this.instances); |
||||||
|
let relations = []; |
||||||
|
forEach(objCopy(this.instances), (instance) => { |
||||||
|
console.log('a instance', instance); |
||||||
|
if (isEmpty(instance.related)) { |
||||||
|
if (!this.model.optional) { |
||||||
|
throw new Error(`Relation "${this.model.name}" is required`) |
||||||
|
} |
||||||
|
console.log("empty related", instance.related); |
||||||
|
return; // continue |
||||||
|
} |
||||||
|
|
||||||
|
let values = []; |
||||||
|
forEach(instance.values, (vv, prop_model_id) => { |
||||||
|
for (let v of vv) { |
||||||
|
v.model_id = castId(prop_model_id); |
||||||
|
values.push(v); |
||||||
|
} |
||||||
|
}) |
||||||
|
instance.model_id = this.model.id; |
||||||
|
instance.values = values; |
||||||
|
relations.push(instance); |
||||||
|
}) |
||||||
|
console.log('collected', relations); |
||||||
|
return relations; |
||||||
|
}, |
||||||
|
|
||||||
|
addInstance() { |
||||||
|
console.log('Add instance'); |
||||||
|
let values = {}; |
||||||
|
forEach(this.properties, (p) => { |
||||||
|
if (p.optional) { |
||||||
|
values[p.id] = []; |
||||||
|
} else { |
||||||
|
values[p.id] = [{id: null, value: objCopy(p.default)}]; |
||||||
|
} |
||||||
|
}); |
||||||
|
this.instances.push({ |
||||||
|
id: null, |
||||||
|
related: '', |
||||||
|
values |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
removeInstance(ri) { |
||||||
|
this.instances.splice(ri, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.new-relation { |
||||||
|
border: 1px dashed gray; |
||||||
|
margin: 10px 0; |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="new-relation" v-for="(instance, ri) in instances" :key="ri"> |
||||||
|
<b>{{ model.name }} -> {{ related_model.name }} |
||||||
|
<select v-model="instance.related"> |
||||||
|
<option v-for="(name, id) in object_names" :value="id">{{name}}</option> |
||||||
|
</select> |
||||||
|
</b> |
||||||
|
|
||||||
|
<a href="#" v-if="model.multiple || model.optional && instances.length > 0" |
||||||
|
style="margin-left: 5px" |
||||||
|
@click="removeInstance(ri)">X</a> |
||||||
|
|
||||||
|
<table v-if="properties"> |
||||||
|
<edit-property v-for="(property, id) in properties" :model="property" :values="instance.values[id]" :key="id"></edit-property> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
<a href="#" v-if="model.multiple || model.optional && instances.length==0" |
||||||
|
@click="addInstance">Add {{ model.name }} -> {{ related_model.name }}</a><br> |
||||||
|
</template> |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,270 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<title>Edit Recipe • YOPA</title> |
||||||
|
<script src="bundle.js"></script> |
||||||
|
<link rel="stylesheet" href="style.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="content"> |
||||||
|
<div id="edit-object-form"></div> |
||||||
|
|
||||||
|
<script> |
||||||
|
onLoad(() => { |
||||||
|
window.app = Yopa.editObjectForm({ |
||||||
|
"model_id": 0, |
||||||
|
// this is objects that can be chosen as related |
||||||
|
"objects": [ |
||||||
|
{ |
||||||
|
"id": 11, |
||||||
|
"model": 2, |
||||||
|
"name": "Lemon" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 12, |
||||||
|
"model": 2, |
||||||
|
"name": "Custard" |
||||||
|
} |
||||||
|
], |
||||||
|
// schema, possibly restricted to the relevant entries |
||||||
|
"schema": { |
||||||
|
"obj_models": [ |
||||||
|
{ |
||||||
|
"id": 2, |
||||||
|
"name": "Ingredient" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 0, |
||||||
|
"name": "Recipe" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 1, |
||||||
|
"name": "Book" |
||||||
|
} |
||||||
|
], |
||||||
|
"prop_models": [ |
||||||
|
{ |
||||||
|
"data_type": "Integer", |
||||||
|
"default": { |
||||||
|
"Integer": 0 |
||||||
|
}, |
||||||
|
"id": 13, |
||||||
|
"multiple": false, |
||||||
|
"name": "Number", |
||||||
|
"object": 0, |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "String", |
||||||
|
"default": { |
||||||
|
"String": "" |
||||||
|
}, |
||||||
|
"id": 17, |
||||||
|
"multiple": true, |
||||||
|
"name": "MultiString", |
||||||
|
"object": 0, |
||||||
|
"optional": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "Integer", |
||||||
|
"default": { |
||||||
|
"Integer": 0 |
||||||
|
}, |
||||||
|
"id": 7, |
||||||
|
"multiple": false, |
||||||
|
"name": "page", |
||||||
|
"object": 6, |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "String", |
||||||
|
"default": { |
||||||
|
"String": "" |
||||||
|
}, |
||||||
|
"id": 18, |
||||||
|
"multiple": true, |
||||||
|
"name": "OptiMultiString", |
||||||
|
"object": 0, |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "Boolean", |
||||||
|
"default": { |
||||||
|
"Boolean": false |
||||||
|
}, |
||||||
|
"id": 14, |
||||||
|
"multiple": false, |
||||||
|
"name": "Bool", |
||||||
|
"object": 0, |
||||||
|
"optional": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "String", |
||||||
|
"default": { |
||||||
|
"String": "" |
||||||
|
}, |
||||||
|
"id": 15, |
||||||
|
"multiple": false, |
||||||
|
"name": "String", |
||||||
|
"object": 0, |
||||||
|
"optional": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "Decimal", |
||||||
|
"default": { |
||||||
|
"Decimal": 0.0 |
||||||
|
}, |
||||||
|
"id": 16, |
||||||
|
"multiple": false, |
||||||
|
"name": "Float", |
||||||
|
"object": 0, |
||||||
|
"optional": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"data_type": "String", |
||||||
|
"default": { |
||||||
|
"String": "" |
||||||
|
}, |
||||||
|
"id": 10, |
||||||
|
"multiple": false, |
||||||
|
"name": "qty", |
||||||
|
"object": 9, |
||||||
|
"optional": true |
||||||
|
} |
||||||
|
], |
||||||
|
"rel_models": [ |
||||||
|
{ |
||||||
|
"id": 6, |
||||||
|
"multiple": true, |
||||||
|
"name": "book reference", |
||||||
|
"object": 0, |
||||||
|
"optional": true, |
||||||
|
"reciprocal_name": "recipes", |
||||||
|
"related": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 8, |
||||||
|
"multiple": true, |
||||||
|
"name": "related recipe", |
||||||
|
"object": 0, |
||||||
|
"optional": true, |
||||||
|
"reciprocal_name": "related recipe", |
||||||
|
"related": 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 9, |
||||||
|
"multiple": true, |
||||||
|
"name": "ingredient", |
||||||
|
"object": 0, |
||||||
|
"optional": true, |
||||||
|
"reciprocal_name": "recipes", |
||||||
|
"related": 2 |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"object": { |
||||||
|
"id": 19, |
||||||
|
"model": 0, |
||||||
|
"name": "Custard with lemon", |
||||||
|
"values": { |
||||||
|
"14": [{ |
||||||
|
"id": 25, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"Boolean": true |
||||||
|
} |
||||||
|
}], |
||||||
|
"17": [{ |
||||||
|
"id": 21, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"String": "Bla" |
||||||
|
} |
||||||
|
}, { |
||||||
|
"id": 22, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"String": "Ble" |
||||||
|
} |
||||||
|
}, { |
||||||
|
"id": 23, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"String": "Bli" |
||||||
|
} |
||||||
|
}], |
||||||
|
"15": [{ |
||||||
|
"id": 26, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"String": "Bla" |
||||||
|
} |
||||||
|
}], |
||||||
|
"16": [{ |
||||||
|
"id": 27, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"Decimal": 15.6 |
||||||
|
} |
||||||
|
}], |
||||||
|
"13": [{ |
||||||
|
"id": 20, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"Integer": 15 |
||||||
|
} |
||||||
|
}], |
||||||
|
"18": [{ |
||||||
|
"id": 24, |
||||||
|
"object": 19, |
||||||
|
"value": { |
||||||
|
"String": "sdfsfsdfsdf" |
||||||
|
} |
||||||
|
}] |
||||||
|
}, |
||||||
|
"relations": { |
||||||
|
"6": [], |
||||||
|
"8": [], |
||||||
|
"9": [ |
||||||
|
{ |
||||||
|
"id": 28, |
||||||
|
"object": 19, |
||||||
|
"model": 9, |
||||||
|
"related": 11, |
||||||
|
"values": { |
||||||
|
"10": [{ |
||||||
|
"id": 29, |
||||||
|
"object": 28, |
||||||
|
// model:10 |
||||||
|
"value": { |
||||||
|
"String": "1" |
||||||
|
} |
||||||
|
}], |
||||||
|
} |
||||||
|
}, { |
||||||
|
"id": 30, |
||||||
|
"object": 19, |
||||||
|
"model": 9, |
||||||
|
"related": 12, |
||||||
|
"values": { |
||||||
|
"10": [{ |
||||||
|
"id": 31, |
||||||
|
"object": 30, |
||||||
|
// model:10 |
||||||
|
"value": { |
||||||
|
"String": "2" |
||||||
|
} |
||||||
|
}], |
||||||
|
} |
||||||
|
} |
||||||
|
], |
||||||
|
}, |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
|
</script> |
||||||
|
</div> |
||||||
|
</body> |
||||||
|
</html> |
Loading…
Reference in new issue