wip new object editor form with a test html file

master
Ondřej Hruška 3 years ago
parent b24a8b2805
commit 95381c1da3
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 161
      yopa-web/resources/src/components/EditObjectForm.vue
  2. 62
      yopa-web/resources/src/components/EditPropertyField.vue
  3. 121
      yopa-web/resources/src/components/EditRelationForm.vue
  4. 3
      yopa-web/resources/src/components/NewObjectForm.vue
  5. 3
      yopa-web/resources/src/components/NewRelationForm.vue
  6. 34
      yopa-web/resources/src/main.js
  7. 19
      yopa-web/resources/src/utils.js
  8. 9188
      yopa-web/resources/static/bundle.js
  9. 2
      yopa-web/resources/static/bundle.js.map
  10. 5
      yopa-web/resources/static/style.css
  11. 270
      yopa-web/resources/static/test_form.html

@ -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 }} -&gt; {{ 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 }} -&gt; {{ 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 &bull; 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…
Cancel
Save