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