parent
780ad36696
commit
b5a4900209
@ -0,0 +1 @@ |
|||||||
|
node_modules/ |
@ -1,64 +0,0 @@ |
|||||||
{% extends "_layout" %} |
|
||||||
|
|
||||||
{% block title -%} |
|
||||||
Index |
|
||||||
{%- endblock %} |
|
||||||
|
|
||||||
{% block nav -%} |
|
||||||
<a href="/">Home</a> |
|
||||||
{%- endblock %} |
|
||||||
|
|
||||||
{% block content -%} |
|
||||||
|
|
||||||
<h1>Welcome to tera on actix</h1> |
|
||||||
|
|
||||||
<a href="/model/object/create">New model</a> |
|
||||||
|
|
||||||
<h2>Defined models:</h2> |
|
||||||
|
|
||||||
<ul> |
|
||||||
{% for model in models %} |
|
||||||
<li> |
|
||||||
<b>{{model.name}}</b><br> |
|
||||||
|
|
||||||
{# |
|
||||||
|
|
||||||
{% if !model.properties.is_empty() %} |
|
||||||
Properties: |
|
||||||
<ul> |
|
||||||
{% for prop in model.properties %} |
|
||||||
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endif %} |
|
||||||
|
|
||||||
<a href="/model/property/create/{{model.id}}">New property</a> |
|
||||||
|
|
||||||
{% if !model.relations.is_empty() %} |
|
||||||
Relations: |
|
||||||
<ul> |
|
||||||
{% for rel in model.relations %} |
|
||||||
<li> |
|
||||||
{{rel.name}} -> {{rel.related_name}} |
|
||||||
|
|
||||||
{% if !rel.properties.is_empty() %} |
|
||||||
Properties: |
|
||||||
<ul> |
|
||||||
{% for prop in rel.properties %} |
|
||||||
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endif %} |
|
||||||
</li> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endif %} |
|
||||||
|
|
||||||
<a href="/model/relation/create/{{model.id}}">New relation</a> |
|
||||||
|
|
||||||
#} |
|
||||||
</li> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
|
|
||||||
{%- endblock %} |
|
@ -0,0 +1,31 @@ |
|||||||
|
{ |
||||||
|
"name": "rollup-test", |
||||||
|
"version": "1.0.0", |
||||||
|
"main": "index.js", |
||||||
|
"license": "proprietary", |
||||||
|
"devDependencies": { |
||||||
|
"@rollup/plugin-alias": "^2.2.0", |
||||||
|
"@rollup/plugin-commonjs": "^17.0.0", |
||||||
|
"@rollup/plugin-node-resolve": "^11.1.0", |
||||||
|
"@rollup/plugin-replace": "^2.2.0", |
||||||
|
"@vue/compiler-sfc": "^3.0.5", |
||||||
|
"npm-run-all": "^4.1.5", |
||||||
|
"rollup": "^2.36.2", |
||||||
|
"rollup-plugin-terser": "^7.0.2", |
||||||
|
"rollup-plugin-vue": "^6.0.0-beta.10", |
||||||
|
"@rollup/plugin-json": "4.1", |
||||||
|
"serve": "^11.3.2" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"build": "rollup -c", |
||||||
|
"watch": "rollup -c -w", |
||||||
|
"dev": "npm-run-all --parallel start watch", |
||||||
|
"start": "serve public" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"axios": "^0.21.1", |
||||||
|
"lodash-es": "^4.17.20", |
||||||
|
"rollup-plugin-scss": "^2.6.1", |
||||||
|
"vue": "^3.0.5" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import pluginNodeResolve from '@rollup/plugin-node-resolve'; |
||||||
|
import pluginCommonJs from '@rollup/plugin-commonjs'; |
||||||
|
import { terser } from 'rollup-plugin-terser'; |
||||||
|
import rollupReplace from '@rollup/plugin-replace'; |
||||||
|
import pluginAlias from '@rollup/plugin-alias'; |
||||||
|
import pluginJson from '@rollup/plugin-json'; |
||||||
|
import pluginVue from 'rollup-plugin-vue'; |
||||||
|
import pluginScss from 'rollup-plugin-scss'; |
||||||
|
|
||||||
|
// `npm run build` -> `production` is true
|
||||||
|
// `npm run dev` -> `production` is false
|
||||||
|
const production = !process.env.ROLLUP_WATCH; |
||||||
|
|
||||||
|
export default { |
||||||
|
input: 'src/main.js', |
||||||
|
output: { |
||||||
|
file: 'static/bundle.js', |
||||||
|
format: 'iife', // immediately-invoked function expression — suitable for <script> tags
|
||||||
|
sourcemap: true |
||||||
|
}, |
||||||
|
plugins: [ |
||||||
|
pluginAlias({ |
||||||
|
entries: { |
||||||
|
//'vue': __dirname + "/node_modules/vue/dist/vue.esm-bundler.js"
|
||||||
|
} |
||||||
|
}), |
||||||
|
pluginVue({ |
||||||
|
target: 'browser', |
||||||
|
}), |
||||||
|
pluginScss({ |
||||||
|
output: 'static/style.css', |
||||||
|
}), |
||||||
|
pluginJson(), |
||||||
|
pluginNodeResolve({ |
||||||
|
jsnext: true, |
||||||
|
main: true, |
||||||
|
module: true, |
||||||
|
browser: true, |
||||||
|
preferBuiltins: false, |
||||||
|
}), |
||||||
|
pluginCommonJs({ |
||||||
|
include: 'node_modules/**', |
||||||
|
browser: true, |
||||||
|
preferBuiltins: false, |
||||||
|
// ignoreGlobal: false,
|
||||||
|
sourceMap: !production |
||||||
|
}), |
||||||
|
rollupReplace({ |
||||||
|
'process.env.NODE_ENV': production ? '"development"': '"production"', |
||||||
|
'__VUE_OPTIONS_API__': 'true', |
||||||
|
'__VUE_PROD_DEVTOOLS__': 'false', |
||||||
|
}), |
||||||
|
production && terser(), |
||||||
|
] |
||||||
|
}; |
@ -0,0 +1,31 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "BooleanValue", |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
value: Object |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
focus() { |
||||||
|
this.$refs.input.focus(); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
inputValue: { |
||||||
|
set(value) { |
||||||
|
this.value.Boolean = !!value; |
||||||
|
}, |
||||||
|
get() { |
||||||
|
return this.value.Boolean; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input ref="input" type="checkbox" :id="id" value="true" v-model="inputValue"> |
||||||
|
</template> |
@ -0,0 +1,31 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "DecimalValue", |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
value: Object |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
focus() { |
||||||
|
this.$refs.input.focus(); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
inputValue: { |
||||||
|
set(value) { |
||||||
|
this.value.Decimal = parseFloat(value); |
||||||
|
}, |
||||||
|
get() { |
||||||
|
return this.value.Decimal; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input ref="input" type="number" :id="id" step="any" v-model="inputValue"> |
||||||
|
</template> |
@ -0,0 +1,31 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "IntegerValue", |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
value: Object |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
focus() { |
||||||
|
this.$refs.input.focus(); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
inputValue: { |
||||||
|
set(value) { |
||||||
|
this.value.Integer = parseInt(value); |
||||||
|
}, |
||||||
|
get() { |
||||||
|
return this.value.Integer; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input ref="input" type="number" :id="id" step="1" v-model="inputValue"> |
||||||
|
</template> |
@ -0,0 +1,141 @@ |
|||||||
|
<script> |
||||||
|
import {castId, keyBy, objCopy} from "../utils"; |
||||||
|
import forEach from "lodash-es/forEach"; |
||||||
|
import isEmpty from "lodash-es/isEmpty"; |
||||||
|
import axios from "axios"; |
||||||
|
|
||||||
|
export default { |
||||||
|
props: ['model_id', 'schema', 'objects'], |
||||||
|
name: "NewObjectForm", |
||||||
|
data() { |
||||||
|
const model = this.schema.obj_models.find((m) => m.id === this.model_id); |
||||||
|
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) => { |
||||||
|
if (p.optional) { |
||||||
|
values[p.id] = []; |
||||||
|
} else { |
||||||
|
values[p.id] = [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, |
||||||
|
model_names, |
||||||
|
values, |
||||||
|
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, k) => { |
||||||
|
for (let v of vv) { |
||||||
|
values.push({ |
||||||
|
model_id: castId(k), |
||||||
|
value: v |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let relations = []; |
||||||
|
for (let rref of this.relationRefs) { |
||||||
|
for (let r of rref.collectData()) { |
||||||
|
relations.push(r); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
model_id: this.model_id, // string is fine |
||||||
|
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/create', |
||||||
|
data: data |
||||||
|
}) |
||||||
|
.then(function (response) { |
||||||
|
console.log('Response', response); |
||||||
|
}) |
||||||
|
.catch(function (error) { |
||||||
|
console.log('Error', error); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setRelationRef(el) { |
||||||
|
if (el) { |
||||||
|
this.relationRefs.push(el) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
beforeUpdate() { |
||||||
|
this.relationRefs = [] |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$el.parentNode |
||||||
|
.classList.add('EditForm'); |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<h2>New {{ 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> |
||||||
|
|
||||||
|
<property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></property> |
||||||
|
</table> |
||||||
|
|
||||||
|
<h3>Relations</h3> |
||||||
|
|
||||||
|
<new-relation |
||||||
|
v-for="relation in relations" |
||||||
|
:ref="setRelationRef" |
||||||
|
:model_id="relation.id" |
||||||
|
:objects="objects" |
||||||
|
:schema="schema" |
||||||
|
></new-relation> |
||||||
|
</template> |
@ -0,0 +1,141 @@ |
|||||||
|
<script> |
||||||
|
import {castId, keyBy, objCopy} from "../utils"; |
||||||
|
import forEach from "lodash-es/forEach"; |
||||||
|
import isEmpty from "lodash-es/isEmpty"; |
||||||
|
|
||||||
|
export default { |
||||||
|
props: ['model_id', 'schema', 'objects'], |
||||||
|
name: "NewRelationForm", |
||||||
|
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; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
let instances = []; |
||||||
|
if (!model.optional) { |
||||||
|
// TODO avoid duplicated code |
||||||
|
let values = {}; |
||||||
|
forEach(this.properties, (p) => { |
||||||
|
if (p.optional) { |
||||||
|
values[p.id] = []; |
||||||
|
} else { |
||||||
|
values[p.id] = [objCopy(p.default)]; |
||||||
|
} |
||||||
|
}); |
||||||
|
instances.push({ |
||||||
|
related: '', |
||||||
|
values |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
model, |
||||||
|
related_model, |
||||||
|
properties, |
||||||
|
object_names: choices, |
||||||
|
instances, |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
collectData() { |
||||||
|
let relations = []; |
||||||
|
forEach(objCopy(this.instances), (instance) => { |
||||||
|
if (isEmpty(instance.related)) { |
||||||
|
if (!this.model.optional) { |
||||||
|
throw new Error(`Relation "${this.model.name}" is required`) |
||||||
|
} |
||||||
|
return; // continue |
||||||
|
} |
||||||
|
|
||||||
|
let values = []; |
||||||
|
forEach(instance.values, (vv, prop_model_id) => { |
||||||
|
|
||||||
|
for (let v of vv) { |
||||||
|
values.push({ |
||||||
|
model_id: castId(prop_model_id), |
||||||
|
value: v |
||||||
|
}); |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
relations.push({ |
||||||
|
model_id: this.model.id, |
||||||
|
related_id: castId(instance.related), |
||||||
|
values |
||||||
|
}); |
||||||
|
}) |
||||||
|
return relations; |
||||||
|
}, |
||||||
|
|
||||||
|
addInstance() { |
||||||
|
let values = {}; |
||||||
|
forEach(this.properties, (p) => { |
||||||
|
if (p.optional) { |
||||||
|
values[p.id] = []; |
||||||
|
} else { |
||||||
|
values[p.id] = [objCopy(p.default)]; |
||||||
|
} |
||||||
|
}); |
||||||
|
this.instances.push({ |
||||||
|
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> |
||||||
|
<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"> |
||||||
|
<property v-for="(property, id) in properties" :model="property" :values="instance.values[id]" :key="id"></property> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
<a href="#" v-if="model.multiple || model.optional && instances.length==0" |
||||||
|
@click="addInstance">Add {{ model.name }} -> {{ related_model.name }}</a> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,59 @@ |
|||||||
|
<script> |
||||||
|
import {objCopy, uniqueId} from "../utils"; |
||||||
|
import * as Vue from 'vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "PropertyField", |
||||||
|
props: ['model', 'values'], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
id: uniqueId(), |
||||||
|
fieldRefs: [], |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
addValue(event) { |
||||||
|
this.values.push(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="(value, 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="value" :id="vi===0?id:null" v-if="model.data_type==='String'"></string-value> |
||||||
|
<integer-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='Integer'"></integer-value> |
||||||
|
<decimal-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='Decimal'"></decimal-value> |
||||||
|
<boolean-value :ref="setFieldRef" :value="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,31 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "StringValue", |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
value: Object |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
focus() { |
||||||
|
this.$refs.input.focus(); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
inputValue: { |
||||||
|
set(value) { |
||||||
|
this.value.String = value |
||||||
|
}, |
||||||
|
get() { |
||||||
|
return this.value.String; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input ref="input" type="text" :id="id" v-model="inputValue"> |
||||||
|
</template> |
@ -0,0 +1,36 @@ |
|||||||
|
import * as Vue from "vue"; |
||||||
|
|
||||||
|
import './style/app.scss'; |
||||||
|
|
||||||
|
import StringValue from "./components/StringValue.vue"; |
||||||
|
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"; |
||||||
|
|
||||||
|
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) { |
||||||
|
document.addEventListener('DOMContentLoaded', callback); |
||||||
|
} |
||||||
|
|
||||||
|
window.Yopa = { |
||||||
|
newObjectForm(opts) { |
||||||
|
// Opts: model_id, schema, objects (named objects for relations)
|
||||||
|
let app = window.app = Vue.createApp(NewObjectForm, opts); |
||||||
|
registerComponents(app); |
||||||
|
let instance = app.mount('#new-object-form'); |
||||||
|
|
||||||
|
// ...
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,222 @@ |
|||||||
|
*, *::before, *::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
html, textarea, select { |
||||||
|
font-family: "IBM Plex", "DejaVu Sans", "Helvetica", sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
.Form { |
||||||
|
display: block; |
||||||
|
width: 900px; |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
|
||||||
|
nav.top-nav { |
||||||
|
margin-bottom: .5rem; |
||||||
|
border-bottom: 1px solid silver; |
||||||
|
} |
||||||
|
|
||||||
|
nav.top-nav, .content { |
||||||
|
margin: 0 auto; |
||||||
|
width: 900px; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: gray; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
a:hover { |
||||||
|
color: black; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
|
||||||
|
nav.top-nav a { |
||||||
|
display: inline-block; |
||||||
|
padding: .75rem; |
||||||
|
|
||||||
|
color: gray; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
nav.top-nav a:hover { |
||||||
|
color: black; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
|
||||||
|
.Form .Row { |
||||||
|
display: flex; |
||||||
|
padding: .25rem; |
||||||
|
} |
||||||
|
|
||||||
|
.Form .Row.indented { |
||||||
|
padding-left: 10.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
input[type="text"], |
||||||
|
input[type="number"], |
||||||
|
textarea, |
||||||
|
.tag-input { |
||||||
|
border: 1px solid silver; |
||||||
|
padding: 0.5rem; |
||||||
|
border-radius: 5px; |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
input[type="text"]:focus, |
||||||
|
input[type="number"]:focus, |
||||||
|
textarea:focus, |
||||||
|
.tag-input.active { |
||||||
|
box-shadow: inset 0 0 0 1px #3c97ff; |
||||||
|
border-color: #3c97ff; |
||||||
|
outline: 0 none !important; |
||||||
|
} |
||||||
|
|
||||||
|
.Form label { |
||||||
|
flex-shrink: 0; |
||||||
|
width: 10rem; |
||||||
|
height: 2.1rem; |
||||||
|
line-height: 2.1rem; |
||||||
|
vertical-align: middle; |
||||||
|
text-align: right; |
||||||
|
display: inline-block; |
||||||
|
padding-right: .5rem; |
||||||
|
align-self: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.Form input[type="text"], |
||||||
|
.Form input[type="number"], |
||||||
|
.Form select { |
||||||
|
height: 2.1rem; |
||||||
|
width: 15rem; |
||||||
|
} |
||||||
|
|
||||||
|
.Form textarea { |
||||||
|
flex-shrink: 1; |
||||||
|
width: 30rem; |
||||||
|
height: 6rem; |
||||||
|
} |
||||||
|
|
||||||
|
.Form label.checkbox-wrap { |
||||||
|
width: 15rem; |
||||||
|
padding-right: 1rem; |
||||||
|
text-align: left !important; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-input { |
||||||
|
position: relative; |
||||||
|
width: 30rem; |
||||||
|
padding-bottom: 0rem !important; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-input input, |
||||||
|
.tag-input input:focus { |
||||||
|
border: 0 transparent; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
box-shadow: none; |
||||||
|
outline: 0 none !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
|
||||||
|
.cards-table { |
||||||
|
border-collapse: collapse; |
||||||
|
margin: 0 auto; |
||||||
|
margin-top: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table .actions { |
||||||
|
font-size: 90%; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table td, |
||||||
|
.cards-table th { |
||||||
|
padding: .5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table thead th { |
||||||
|
border-bottom: 2px solid silver; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table tbody td { |
||||||
|
border-bottom: 1px solid silver; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table tbody tr:last-child td { |
||||||
|
border-bottom: 2px solid silver; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table td.tags { |
||||||
|
padding: .25rem; |
||||||
|
} |
||||||
|
|
||||||
|
.cards-table .tag { |
||||||
|
background: #E2E1DF; |
||||||
|
font-size: 90%; |
||||||
|
padding: 0.25rem .45rem; |
||||||
|
border-radius: 3px; |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate { |
||||||
|
margin: 1rem auto; |
||||||
|
width: 300px; |
||||||
|
text-align: center; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate span, |
||||||
|
.paginate a { |
||||||
|
padding: .5rem 1rem; |
||||||
|
border-radius: .5rem; |
||||||
|
border: 1px solid silver; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate span.num { |
||||||
|
border: 1px solid #ccc; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate a { |
||||||
|
cursor: pointer; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate a:hover { |
||||||
|
background: #ccc; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate .disabled { |
||||||
|
opacity: .5; |
||||||
|
cursor: default; |
||||||
|
} |
||||||
|
|
||||||
|
.paginate .disabled:hover { |
||||||
|
color: gray; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
li { |
||||||
|
padding-bottom: .5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.toast { |
||||||
|
border: 1px solid black; |
||||||
|
border-radius: 5px; |
||||||
|
padding: .5rem; |
||||||
|
margin: .5rem 0; |
||||||
|
} |
||||||
|
|
||||||
|
.toast.error { |
||||||
|
border-color: #dc143c; |
||||||
|
} |
||||||
|
|
||||||
|
.toast.success { |
||||||
|
border-color: #32cd32; |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
@import "common"; |
||||||
|
|
||||||
|
.EditForm th { |
||||||
|
text-align: left; |
||||||
|
vertical-align: top; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
cursor: pointer; |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
export function uniqueId() { |
||||||
|
return 'f' |
||||||
|
+ Math.random().toString(16).replace('.', '') |
||||||
|
+ (+new Date()).toString(16); |
||||||
|
} |
||||||
|
|
||||||
|
export function keyBy(array, keyfunc) { |
||||||
|
let result = {}; |
||||||
|
for(let item of array) { |
||||||
|
if (typeof keyfunc == 'string') { |
||||||
|
result[item[keyfunc]] = item; |
||||||
|
} else { |
||||||
|
result[keyfunc(item)] = item; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
export function objCopy(object) { |
||||||
|
return JSON.parse(JSON.stringify(object)); |
||||||
|
} |
||||||
|
|
||||||
|
export function castId(id) { |
||||||
|
// TODO no-op after switching to UUIDs
|
||||||
|
return +id; |
||||||
|
} |
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,22 @@ |
|||||||
|
{% extends "_layout" %} |
||||||
|
|
||||||
|
{% block title -%} |
||||||
|
Create {{model.name}} |
||||||
|
{%- endblock %} |
||||||
|
|
||||||
|
{% block nav -%} |
||||||
|
<a href="/">Home</a> |
||||||
|
{%- endblock %} |
||||||
|
|
||||||
|
{% block content -%} |
||||||
|
|
||||||
|
<div id="new-object-form"></div> |
||||||
|
|
||||||
|
<script> |
||||||
|
onLoad(() => { |
||||||
|
// TODO populate dynamically from the database |
||||||
|
window.app = Yopa.newObjectForm({{ form_data | json_encode | safe }}) |
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
{%- endblock %} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,102 @@ |
|||||||
|
use actix_session::Session; |
||||||
|
use actix_web::{Responder, web, HttpResponse}; |
||||||
|
use crate::session_ext::SessionExt; |
||||||
|
use crate::routes::object_model::ObjectModelForm; |
||||||
|
use crate::TERA; |
||||||
|
use crate::tera_ext::TeraExt; |
||||||
|
use yopa::{ID, model}; |
||||||
|
use yopa::data::Object; |
||||||
|
use serde::{Serialize,Deserialize}; |
||||||
|
use yopa::insert::InsertObj; |
||||||
|
use crate::utils::redirect; |
||||||
|
use actix_web::web::Json; |
||||||
|
use serde_json::Value; |
||||||
|
|
||||||
|
// we only need references here, Context serializes everything to Value.
|
||||||
|
// cloning would be a waste of cycles
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Clone)] |
||||||
|
pub struct Schema<'a> { |
||||||
|
pub obj_models: Vec<&'a model::ObjectModel>, |
||||||
|
pub rel_models: Vec<&'a model::RelationModel>, |
||||||
|
pub prop_models: Vec<&'a model::PropertyModel>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Serialize,Debug,Clone)] |
||||||
|
pub struct ObjectCreateData<'a> { |
||||||
|
pub model_id: ID, |
||||||
|
pub schema: Schema<'a>, |
||||||
|
pub objects: Vec<&'a Object>, |
||||||
|
} |
||||||
|
|
||||||
|
#[get("/object/create/{model_id}")] |
||||||
|
pub(crate) async fn create_form( |
||||||
|
model_id: web::Path<ID>, |
||||||
|
store: crate::YopaStoreWrapper, |
||||||
|
session: Session |
||||||
|
) -> actix_web::Result<impl Responder> { |
||||||
|
let mut context = tera::Context::new(); |
||||||
|
session.render_flash(&mut context); |
||||||
|
|
||||||
|
let rg = store.read().await; |
||||||
|
|
||||||
|
let model = rg.get_object_model(*model_id) |
||||||
|
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?; |
||||||
|
|
||||||
|
context.insert("model", model); |
||||||
|
|
||||||
|
let relations : Vec<_> = rg.get_relation_models_for_object(model.id).collect(); |
||||||
|
|
||||||
|
let mut prop_object_ids : Vec<ID> = relations.iter().map(|r| r.id).collect(); |
||||||
|
prop_object_ids.push(model.id); |
||||||
|
|
||||||
|
prop_object_ids.sort(); |
||||||
|
prop_object_ids.dedup(); |
||||||
|
|
||||||
|
let mut related_ids : Vec<_> = relations.iter().map(|r| r.related).collect(); |
||||||
|
|
||||||
|
related_ids.sort(); |
||||||
|
related_ids.dedup(); |
||||||
|
|
||||||
|
let form_data = ObjectCreateData { |
||||||
|
model_id: model.id, |
||||||
|
schema: Schema { |
||||||
|
obj_models: rg.get_object_models().collect(), // TODO get only the ones that matter here
|
||||||
|
rel_models: relations, |
||||||
|
prop_models: rg.get_property_models_for_parents(&prop_object_ids).collect() |
||||||
|
}, |
||||||
|
objects: rg.get_objects_of_type(&related_ids).collect() |
||||||
|
}; |
||||||
|
|
||||||
|
context.insert("form_data", &form_data); |
||||||
|
|
||||||
|
TERA.build_response("objects/object_create", &context) |
||||||
|
} |
||||||
|
|
||||||
|
#[post("/object/create")] |
||||||
|
pub(crate) async fn create( |
||||||
|
form: web::Json<InsertObj>, |
||||||
|
store: crate::YopaStoreWrapper, |
||||||
|
session: Session, |
||||||
|
) -> actix_web::Result<impl Responder> { |
||||||
|
warn!("{:?}", form); |
||||||
|
|
||||||
|
// let des : InsertObj = serde_json::from_value(form.into_inner()).unwrap();
|
||||||
|
//
|
||||||
|
// Ok(HttpResponse::Ok().finish())
|
||||||
|
//
|
||||||
|
let mut wg = store.write().await; |
||||||
|
let form = form.into_inner(); |
||||||
|
let name = form.name.clone(); |
||||||
|
match wg.insert_object(form) { |
||||||
|
Ok(_id) => { |
||||||
|
debug!("Object created, redirecting to root"); |
||||||
|
session.flash_success(format!("Object \"{}\" created.", name)); |
||||||
|
Ok(HttpResponse::Ok().finish()) |
||||||
|
} |
||||||
|
Err(e) => { |
||||||
|
warn!("Error creating model: {}", e); |
||||||
|
Ok(HttpResponse::BadRequest().body(e.to_string())) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue