wip new object editor form with a test html file
This commit is contained in:
@@ -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>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import {castId, keyBy, objCopy} from "../utils";
|
||||
import {castId, keyBy, objCopy, isEmpty} from "../utils";
|
||||
import forEach from "lodash-es/forEach";
|
||||
import isEmpty from "lodash-es/isEmpty";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import {castId, keyBy, objCopy} from "../utils";
|
||||
import {castId, keyBy, objCopy, isEmpty} from "../utils";
|
||||
import forEach from "lodash-es/forEach";
|
||||
import isEmpty from "lodash-es/isEmpty";
|
||||
|
||||
export default {
|
||||
props: ['model_id', 'schema', 'objects'],
|
||||
|
||||
@@ -7,16 +7,19 @@ import DecimalValue from "./components/DecimalValue.vue";
|
||||
import BooleanValue from "./components/BooleanValue.vue";
|
||||
import IntegerValue from "./components/IntegerValue.vue";
|
||||
import PropertyField from "./components/PropertyField.vue";
|
||||
|
||||
import NewObjectForm from "./components/NewObjectForm.vue";
|
||||
import NewRelationForm from "./components/NewRelationForm.vue";
|
||||
import NewRelationForm from "./components/NewRelationForm.vue"
|
||||
|
||||
import EditObjectForm from "./components/EditObjectForm.vue";
|
||||
import EditRelationForm from "./components/EditRelationForm.vue";
|
||||
import EditPropertyField from "./components/EditPropertyField.vue";
|
||||
|
||||
function registerComponents(app) {
|
||||
app.component('string-value', StringValue);
|
||||
app.component('integer-value', IntegerValue);
|
||||
app.component('decimal-value', DecimalValue);
|
||||
app.component('boolean-value', BooleanValue);
|
||||
app.component('property', PropertyField);
|
||||
app.component('new-relation', NewRelationForm);
|
||||
}
|
||||
|
||||
window.onLoad = function (callback) {
|
||||
@@ -28,18 +31,31 @@ window.Yopa = {
|
||||
// Opts: model_id, schema, objects (named objects for relations)
|
||||
let app = window.app = Vue.createApp(NewObjectForm, opts);
|
||||
registerComponents(app);
|
||||
app.component('new-relation', NewRelationForm);
|
||||
app.component('property', PropertyField);
|
||||
let instance = app.mount('#new-object-form');
|
||||
|
||||
// ...
|
||||
return instance;
|
||||
},
|
||||
editObjectForm(opts) {
|
||||
// Opts: model_id, schema, objects (named objects for relations)
|
||||
let app = window.app = Vue.createApp(EditObjectForm, opts);
|
||||
registerComponents(app);
|
||||
app.component('edit-relation', EditRelationForm);
|
||||
app.component('edit-property', EditPropertyField);
|
||||
let instance = app.mount('#edit-object-form');
|
||||
|
||||
// ...
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
setTimeout(() => {
|
||||
let toasts = document.getElementsByClassName('toast');
|
||||
if (toasts.length) {
|
||||
toasts[0].style.display = 'none';
|
||||
}
|
||||
}, 3000)
|
||||
setTimeout(() => {
|
||||
let toasts = document.getElementsByClassName('toast');
|
||||
if (toasts.length) {
|
||||
toasts[0].style.display = 'none';
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import lodash_isEmpty from "lodash-es/isEmpty";
|
||||
|
||||
export function uniqueId() {
|
||||
return 'f'
|
||||
+ Math.random().toString(16).replace('.', '')
|
||||
@@ -24,3 +26,20 @@ export function castId(id) {
|
||||
// TODO no-op after switching to UUIDs
|
||||
return +id;
|
||||
}
|
||||
|
||||
// like _.isEmpty, but less stupid
|
||||
export function isEmpty(object) {
|
||||
if (typeof object == 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (object === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof object == 'string') {
|
||||
return object.length === 0;
|
||||
}
|
||||
|
||||
return lodash_isEmpty(object)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -207,3 +207,8 @@ label {
|
||||
border: 1px dashed gray;
|
||||
margin: 10px 0;
|
||||
padding: 10px; }
|
||||
|
||||
.new-relation[data-v-760e133a] {
|
||||
border: 1px dashed gray;
|
||||
margin: 10px 0;
|
||||
padding: 10px; }
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user