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