add sort keys to things, add options object for "multiline"

master
Ondřej Hruška 3 years ago
parent 17328393aa
commit f240e85c20
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      Cargo.lock
  2. 1
      yopa-web/Cargo.toml
  3. 13
      yopa-web/resources/src/components/EditObjectForm.vue
  4. 17
      yopa-web/resources/src/components/EditRelationForm.vue
  5. 9
      yopa-web/resources/src/components/NewObjectForm.vue
  6. 16
      yopa-web/resources/src/components/NewRelationForm.vue
  7. 24
      yopa-web/resources/src/main.js
  8. 4
      yopa-web/resources/src/style/app.scss
  9. 3
      yopa-web/resources/src/utils.js
  10. 2
      yopa-web/resources/static/bundle.js
  11. 2
      yopa-web/resources/static/bundle.js.map
  12. 3
      yopa-web/resources/static/style.css
  13. 11
      yopa-web/resources/templates/_form_macros.html.tera
  14. 2
      yopa-web/resources/templates/models/model_create.html.tera
  15. 2
      yopa-web/resources/templates/models/model_update.html.tera
  16. 16
      yopa-web/resources/templates/models/property_create.html.tera
  17. 16
      yopa-web/resources/templates/models/property_update.html.tera
  18. 1
      yopa-web/resources/templates/models/relation_create.html.tera
  19. 1
      yopa-web/resources/templates/models/relation_update.html.tera
  20. 2
      yopa-web/src/main.rs
  21. 14
      yopa-web/src/routes/models.rs
  22. 7
      yopa-web/src/routes/models/object.rs
  23. 24
      yopa-web/src/routes/models/property.rs
  24. 7
      yopa-web/src/routes/models/relation.rs
  25. 45
      yopa-web/src/routes/objects.rs
  26. 10
      yopa/src/lib.rs
  27. 102
      yopa/src/model.rs

2
Cargo.lock generated

@ -1233,6 +1233,7 @@ checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -2732,6 +2733,7 @@ dependencies = [
"clap", "clap",
"heck", "heck",
"include_dir", "include_dir",
"indexmap",
"itertools", "itertools",
"json_dotpath", "json_dotpath",
"log", "log",

@ -28,6 +28,7 @@ anyhow = "1.0.38"
thiserror = "1.0.24" thiserror = "1.0.24"
clap = "2" clap = "2"
serde_with = "1.6.4" serde_with = "1.6.4"
indexmap = { version = "1.6.1", features = ["serde-1"] }
tokio = { version="0.2.6", features=["full"] } tokio = { version="0.2.6", features=["full"] }

@ -14,9 +14,6 @@ export default {
let properties = this.schema.prop_models.filter((m) => m.object === 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); 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 = {}; let values = {};
properties.forEach((p) => { properties.forEach((p) => {
let existing = object.values[p.id] || []; let existing = object.values[p.id] || [];
@ -38,8 +35,8 @@ export default {
} }
}); });
properties = keyBy(properties, 'id'); let propertiesById = keyBy(properties, 'id');
relations = keyBy(relations, 'id'); let relationsById = keyBy(relations, 'id');
let model_names = {}; let model_names = {};
this.schema.obj_models.forEach((m) => { this.schema.obj_models.forEach((m) => {
@ -49,7 +46,9 @@ export default {
return { return {
model, model,
properties, properties,
propertiesById,
relations, relations,
relationsById,
haveRelations: !isEmpty(relations), haveRelations: !isEmpty(relations),
model_names, model_names,
values, values,
@ -67,7 +66,7 @@ export default {
let values = []; let values = [];
forEach(objCopy(this.values), (vv, prop_model_id) => { forEach(objCopy(this.values), (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) { if (isEqual(v.value, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
continue; continue;
} }
@ -148,7 +147,7 @@ export default {
</div> </div>
<div class="form-horizontal container"> <div class="form-horizontal container">
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property> <edit-property v-for="property in properties" :model="property" :values="values[property.id]" :key="property.id"></edit-property>
</div> </div>
<div v-if="haveRelations"> <div v-if="haveRelations">

@ -12,12 +12,9 @@ export default {
let properties = this.schema.prop_models.filter((m) => m.object === model.id); let properties = this.schema.prop_models.filter((m) => m.object === model.id);
properties.sort((a, b) => a.name.localeCompare(b.name)); let propertiesById = {};
if (!isEmpty(properties)) {
if (isEmpty(properties)) { propertiesById = keyBy(properties, 'id');
properties = [];
} else {
properties = keyBy(properties, 'id');
} }
let related_model = this.schema.obj_models.find((m) => m.id === model.related); let related_model = this.schema.obj_models.find((m) => m.id === model.related);
@ -35,6 +32,7 @@ export default {
model, model,
related_model, related_model,
properties, properties,
propertiesById,
object_names: choices, object_names: choices,
instances: objCopy(this.initialInstances), instances: objCopy(this.initialInstances),
} }
@ -56,7 +54,7 @@ export default {
let values = []; let values = [];
forEach(instance.values, (vv, prop_model_id) => { forEach(instance.values, (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) { if (isEqual(v.value, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
continue; continue;
} }
v.model = castId(prop_model_id); v.model = castId(prop_model_id);
@ -68,7 +66,6 @@ export default {
instance.values = values; instance.values = values;
relations.push(instance); relations.push(instance);
}) })
console.log('collected', relations);
return relations; return relations;
}, },
@ -112,9 +109,9 @@ export default {
</div> </div>
</div> </div>
<edit-property v-for="(property, id) in properties" <edit-property v-for="property in properties"
:model="property" :model="property"
:values="instance.values[id]" :key="id"></edit-property> :values="instance.values[property.id]" :key="property.id"></edit-property>
</div> </div>
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">

@ -12,9 +12,6 @@ export default {
let properties = this.schema.prop_models.filter((m) => m.object === 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); 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 = {}; let values = {};
properties.forEach((p) => { properties.forEach((p) => {
if (p.optional) { if (p.optional) {
@ -24,8 +21,8 @@ export default {
} }
}); });
properties = keyBy(properties, 'id'); let propertiesById = keyBy(properties, 'id');
relations = keyBy(relations, 'id'); let relationsById = keyBy(relations, 'id');
let model_names = {}; let model_names = {};
this.schema.obj_models.forEach((m) => { this.schema.obj_models.forEach((m) => {
@ -36,6 +33,8 @@ export default {
model, model,
properties, properties,
relations, relations,
propertiesById,
relationsById,
haveRelations: !isEmpty(relations), haveRelations: !isEmpty(relations),
model_names, model_names,
values, values,

@ -12,12 +12,9 @@ export default {
let properties = this.schema.prop_models.filter((m) => m.object === model.id); let properties = this.schema.prop_models.filter((m) => m.object === model.id);
properties.sort((a, b) => a.name.localeCompare(b.name)); let propertiesById = {};
if (!isEmpty(properties)) {
if (isEmpty(properties)) { propertiesById = keyBy(properties, 'id');
properties = [];
} else {
properties = keyBy(properties, 'id');
} }
let related_model = this.schema.obj_models.find((m) => m.id === model.related); let related_model = this.schema.obj_models.find((m) => m.id === model.related);
@ -52,6 +49,7 @@ export default {
model, model,
related_model, related_model,
properties, properties,
propertiesById,
object_names: choices, object_names: choices,
instances, instances,
} }
@ -71,7 +69,7 @@ export default {
forEach(instance.values, (vv, prop_model_id) => { forEach(instance.values, (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
if (isEqual(v, {"String": ""}) && this.properties[prop_model_id].optional) { if (isEqual(v, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
continue; continue;
} }
@ -131,9 +129,9 @@ export default {
</div> </div>
</div> </div>
<property v-for="(property, id) in properties" <property v-for="property in properties"
:model="property" :model="property"
:values="instance.values[id]" :key="id"></property> :values="instance.values[property.id]" :key="property.id"></property>
</div> </div>
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">

@ -6,6 +6,7 @@ import StringValue from "./components/value/StringValue.vue";
import DecimalValue from "./components/value/DecimalValue.vue"; import DecimalValue from "./components/value/DecimalValue.vue";
import BooleanValue from "./components/value/BooleanValue.vue"; import BooleanValue from "./components/value/BooleanValue.vue";
import IntegerValue from "./components/value/IntegerValue.vue"; import IntegerValue from "./components/value/IntegerValue.vue";
import TextValue from "./components/value/TextValue.vue";
import PropertyField from "./components/PropertyField.vue"; import PropertyField from "./components/PropertyField.vue";
import NewObjectForm from "./components/NewObjectForm.vue"; import NewObjectForm from "./components/NewObjectForm.vue";
@ -14,9 +15,11 @@ import NewRelationForm from "./components/NewRelationForm.vue"
import EditObjectForm from "./components/EditObjectForm.vue"; import EditObjectForm from "./components/EditObjectForm.vue";
import EditRelationForm from "./components/EditRelationForm.vue"; import EditRelationForm from "./components/EditRelationForm.vue";
import EditPropertyField from "./components/EditPropertyField.vue"; import EditPropertyField from "./components/EditPropertyField.vue";
import {qs} from "./utils";
function registerComponents(app) { function registerComponents(app) {
app.component('string-value', StringValue); app.component('string-value', StringValue);
app.component('text-value', StringValue);
app.component('integer-value', IntegerValue); app.component('integer-value', IntegerValue);
app.component('decimal-value', DecimalValue); app.component('decimal-value', DecimalValue);
app.component('boolean-value', BooleanValue); app.component('boolean-value', BooleanValue);
@ -48,6 +51,27 @@ window.Yopa = {
// ... // ...
return instance; return instance;
},
propertyEditForm() {
// multiple and unique are XORed. This is also enforced server-side
let multiple = qs('#multiple');
let unique = qs('#unique');
let type = qs('#data_type');
unique.addEventListener('input', function () {
multiple.checked &= !unique.checked;
})
multiple.addEventListener('input', function () {
unique.checked &= !multiple.checked;
})
type.addEventListener('input', function () {
console.log(type.value);
toggleOptionalBoxes();
})
function toggleOptionalBoxes() {
qs('#string-options').classList.toggle('hidden', type.value !== 'String');
}
toggleOptionalBoxes();
} }
}; };

@ -17,3 +17,7 @@ table.object-display {
width: 225px; width: 225px;
} }
} }
.hidden {
display: none !important;
}

@ -43,3 +43,6 @@ export function isEmpty(object) {
return lodash_isEmpty(object) return lodash_isEmpty(object)
} }
export function qs(s) { return document.querySelector(s); }
export function qss(s) { return document.querySelectorAll(s); }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -4129,3 +4129,6 @@ table.object-display {
margin-bottom: 0.6rem; } margin-bottom: 0.6rem; }
table.object-display tbody th { table.object-display tbody th {
width: 225px; } width: 225px; }
.hidden {
display: none !important; }

@ -24,6 +24,17 @@
</div> </div>
{% endmacro input %} {% endmacro input %}
{% macro integer(name, label, value) %}
<div class="form-group cols">
<div class="col-3 pl-2">
<label class="form-label" for="{{name}}">{{label}}</label>
</div>
<div class="col-9 pr-2">
<input type="number" step="1" class="form-input input-inline" id="{{name}}" name="{{name}}" value="{{value}}" autocomplete="off">
</div>
</div>
{% endmacro input %}
{% macro checkbox(name, label, checked) %} {% macro checkbox(name, label, checked) %}
<div class="form-group cols"> <div class="form-group cols">
<div class="col-3 pl-2"> <div class="col-3 pl-2">

@ -16,6 +16,8 @@ Define object
<div class="form-horizontal container"> <div class="form-horizontal container">
{{ form::text(name="name", label="Name", value=old.name) }} {{ form::text(name="name", label="Name", value=old.name) }}
{{ form::integer(name="sort_key", label="Sort order", value=old.sort_key) }}
</div> </div>
</form> </form>

@ -17,6 +17,8 @@ Edit object model
<div class="form-horizontal container"> <div class="form-horizontal container">
{{ form::text(name="name", label="Name", value=model.name) }} {{ form::text(name="name", label="Name", value=model.name) }}
{{ form::integer(name="sort_key", label="Sort order", value=model.sort_key) }}
<div class="form-group cols"> <div class="form-group cols">
<div class="col-3 pl-2"> <div class="col-3 pl-2">
<label class="form-label" for="name_property">Name property</label> <label class="form-label" for="name_property">Name property</label>

@ -38,20 +38,18 @@ Define property
</div> </div>
{{ form::text(name="default", label="Default", value=old.default) }} {{ form::text(name="default", label="Default", value=old.default) }}
{{ form::integer(name="sort_key", label="Sort order", value=old.sort_key) }}
<div class="hidden" id="string-options">
{{ form::checkbox(name="opt_multiline", label="Multi-line", checked=old.opt_multiline) }}
</div>
</div> </div>
</form> </form>
<script> <script>
(function () { (function () {
// multiple and unique are XORed. This is also enforced server-side Yopa.propertyEditForm()
let multiple = document.getElementById('multiple');
let unique = document.getElementById('unique');
unique.addEventListener('input', function () {
multiple.checked &= !unique.checked;
})
multiple.addEventListener('input', function () {
unique.checked &= !multiple.checked;
})
})(); })();
</script> </script>

@ -36,20 +36,18 @@ Edit property
</div> </div>
{{ form::text(name="default", label="Default", value=model.default|print_typed_value) }} {{ form::text(name="default", label="Default", value=model.default|print_typed_value) }}
{{ form::integer(name="sort_key", label="Sort order", value=model.sort_key) }}
<div id="string-options">
{{ form::checkbox(name="opt_multiline", label="Multi-line", checked=model.options.multiline) }}
</div>
</div> </div>
</form> </form>
<script> <script>
(function () { (function () {
// multiple and unique are XORed. This is also enforced server-side Yopa.propertyEditForm()
let multiple = document.getElementById('multiple');
let unique = document.getElementById('unique');
unique.addEventListener('input', function () {
multiple.checked &= !unique.checked;
})
multiple.addEventListener('input', function () {
unique.checked &= !multiple.checked;
})
})(); })();
</script> </script>

@ -21,6 +21,7 @@ Define relation
{{ form::text(name="reciprocal_name", label="Reciprocal name", value=old.reciprocal_name) }} {{ form::text(name="reciprocal_name", label="Reciprocal name", value=old.reciprocal_name) }}
{{ form::checkbox(name="optional", label="Optional", checked=old.optional) }} {{ form::checkbox(name="optional", label="Optional", checked=old.optional) }}
{{ form::checkbox(name="multiple", label="Multiple", checked=old.multiple) }} {{ form::checkbox(name="multiple", label="Multiple", checked=old.multiple) }}
{{ form::integer(name="sort_key", label="Sort order", value=old.sort_key) }}
<div class="form-group cols"> <div class="form-group cols">
<div class="col-3 pl-2"> <div class="col-3 pl-2">

@ -19,6 +19,7 @@ Edit relation
{{ form::text(name="reciprocal_name", label="Reciprocal name", value=model.reciprocal_name) }} {{ form::text(name="reciprocal_name", label="Reciprocal name", value=model.reciprocal_name) }}
{{ form::checkbox(name="optional", label="Optional", checked=model.optional) }} {{ form::checkbox(name="optional", label="Optional", checked=model.optional) }}
{{ form::checkbox(name="multiple", label="Multiple", checked=model.multiple) }} {{ form::checkbox(name="multiple", label="Multiple", checked=model.multiple) }}
{{ form::integer(name="sort_key", label="Sort order", value=model.sort_key) }}
</div> </div>
<p>The related object cannot be changed. Create a new relation if needed.</p> <p>The related object cannot be changed. Create a new relation if needed.</p>

@ -4,6 +4,8 @@ extern crate actix_web;
extern crate log; extern crate log;
#[macro_use] #[macro_use]
extern crate thiserror; extern crate thiserror;
#[macro_use]
extern crate serde_json;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;

@ -5,6 +5,7 @@ use crate::tera_ext::TeraExt;
use crate::TERA; use crate::TERA;
use actix_session::Session; use actix_session::Session;
use actix_web::Responder; use actix_web::Responder;
use yopa::model::{PropertyModel, RelationModel, ObjectModel};
pub(crate) mod object; pub(crate) mod object;
pub(crate) mod property; pub(crate) mod property;
@ -32,7 +33,7 @@ pub(crate) async fn list(
.into_iter() .into_iter()
.map(|rm| { .map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default(); let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name); rprops.sort_by(PropertyModel::order_refs);
RelationModelDisplay { RelationModelDisplay {
model: rm, model: rm,
@ -41,7 +42,7 @@ pub(crate) async fn list(
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
relations.sort_by_key(|d| &d.model.name); relations.sort_by(|a, b| RelationModel::order_refs(&a.model, &b.model));
// Relations coming INTO this model // Relations coming INTO this model
let reciprocal_relations = model_rec_relations.remove(&om.id).unwrap_or_default(); let reciprocal_relations = model_rec_relations.remove(&om.id).unwrap_or_default();
@ -49,7 +50,7 @@ pub(crate) async fn list(
.into_iter() .into_iter()
.map(|rm| { .map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default(); let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name); rprops.sort_by(PropertyModel::order_refs);
RelationModelDisplay { RelationModelDisplay {
model: rm, model: rm,
@ -58,21 +59,22 @@ pub(crate) async fn list(
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name); reciprocal_relations.sort_by(|a, b| RelationModel::reciprocal_order_refs(&a.model, &b.model));
let mut properties = model_props.remove(&om.id).unwrap_or_default(); let mut properties = model_props.remove(&om.id).unwrap_or_default();
properties.sort_by_key(|m| &m.name); properties.sort_by(PropertyModel::order_refs);
models.push(ObjectModelDisplay { models.push(ObjectModelDisplay {
id: om.id, id: om.id,
name: &om.name, name: &om.name,
model: &om,
properties, properties,
relations, relations,
reciprocal_relations, reciprocal_relations,
}) })
} }
models.sort_by_key(|m| m.name); models.sort_by(|a, b| ObjectModel::order_refs(&a.model, &b.model));
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("models", &models); ctx.insert("models", &models);

@ -16,6 +16,8 @@ use itertools::Itertools;
pub(crate) struct ObjectModelDisplay<'a> { pub(crate) struct ObjectModelDisplay<'a> {
pub(crate) id: yopa::ID, pub(crate) id: yopa::ID,
pub(crate) name: &'a str, pub(crate) name: &'a str,
#[serde(skip)]
pub(crate) model : &'a ObjectModel,
pub(crate) properties: Vec<&'a PropertyModel>, pub(crate) properties: Vec<&'a PropertyModel>,
pub(crate) relations: Vec<RelationModelDisplay<'a>>, pub(crate) relations: Vec<RelationModelDisplay<'a>>,
pub(crate) reciprocal_relations: Vec<RelationModelDisplay<'a>>, pub(crate) reciprocal_relations: Vec<RelationModelDisplay<'a>>,
@ -43,6 +45,8 @@ pub(crate) struct ObjectModelForm {
// #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected // #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected
#[serde(with = "my_string_empty_as_none")] #[serde(with = "my_string_empty_as_none")]
pub name_property: Option<ID>, pub name_property: Option<ID>,
#[serde(default)]
pub sort_key: i64,
} }
#[post("/model/object/create")] #[post("/model/object/create")]
@ -57,6 +61,7 @@ pub(crate) async fn create(
id: Default::default(), id: Default::default(),
name: form.name.clone(), name: form.name.clone(),
name_property: form.name_property, name_property: form.name_property,
sort_key: form.sort_key
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
@ -101,6 +106,7 @@ pub(crate) async fn update_form(
&ObjectModelForm { &ObjectModelForm {
name: model.name.to_string(), name: model.name.to_string(),
name_property: model.name_property, name_property: model.name_property,
sort_key: model.sort_key,
}, },
); );
} }
@ -127,6 +133,7 @@ pub(crate) async fn update(
id, id,
name: form.name.clone(), name: form.name.clone(),
name_property: form.name_property, name_property: form.name_property,
sort_key: form.sort_key,
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;

@ -2,7 +2,7 @@ use actix_session::Session;
use actix_web::{web, Responder}; use actix_web::{web, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yopa::model::PropertyModel; use yopa::model::{PropertyModel, PropertyOptions};
use yopa::{DataType, TypedValue, ID}; use yopa::{DataType, TypedValue, ID};
use crate::routes::models::relation::ObjectOrRelationModelDisplay; use crate::routes::models::relation::ObjectOrRelationModelDisplay;
@ -37,6 +37,8 @@ pub(crate) async fn create_form(
unique: false, unique: false,
data_type: DataType::String, data_type: DataType::String,
default: "".to_string(), default: "".to_string(),
sort_key: 1000, // big number so it goes at the end by default
opt_multiline: false,
}, },
); );
} }
@ -78,6 +80,10 @@ pub(crate) struct PropertyModelCreateForm {
/// Default value to be parsed to the data type /// Default value to be parsed to the data type
/// May be unused if empty and optional /// May be unused if empty and optional
pub default: String, pub default: String,
#[serde(default)]
pub sort_key: i64,
#[serde(default)]
pub opt_multiline: bool,
} }
fn parse_default(data_type: DataType, default: String) -> Result<TypedValue, String> { fn parse_default(data_type: DataType, default: String) -> Result<TypedValue, String> {
@ -151,6 +157,10 @@ pub(crate) async fn create(
unique, unique,
data_type: form.data_type, data_type: form.data_type,
default, default,
sort_key: form.sort_key,
options: PropertyOptions {
multiline: form.opt_multiline
}
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
@ -202,6 +212,10 @@ pub(crate) struct PropertyModelEditForm {
/// Default value to be parsed to the data type /// Default value to be parsed to the data type
/// May be unused if empty and optional /// May be unused if empty and optional
pub default: String, pub default: String,
#[serde(default)]
pub sort_key: i64,
#[serde(default)]
pub opt_multiline: bool,
} }
#[get("/model/property/update/{model_id}")] #[get("/model/property/update/{model_id}")]
@ -227,9 +241,11 @@ pub(crate) async fn update_form(
model.optional = form.optional; model.optional = form.optional;
model.multiple = form.multiple; model.multiple = form.multiple;
model.unique = form.unique; model.unique = form.unique;
model.sort_key = form.sort_key;
model.options.multiline = form.opt_multiline;
context.insert("model", &model); context.insert("model", &model);
} else { } else {
context.insert("model", model); context.insert("model", &model);
} }
TERA.build_response("models/property_update", &context) TERA.build_response("models/property_update", &context)
@ -265,6 +281,10 @@ pub(crate) async fn update(
unique: form.unique, unique: form.unique,
data_type: form.data_type, data_type: form.data_type,
default, default,
sort_key: form.sort_key,
options: PropertyOptions {
multiline: form.opt_multiline
}
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;

@ -42,6 +42,7 @@ pub(crate) async fn create_form(
optional: false, optional: false,
multiple: false, multiple: false,
related: Default::default(), related: Default::default(),
sort_key: 1000
}, },
); );
} }
@ -70,6 +71,8 @@ pub(crate) struct RelationModelCreateForm {
#[serde(default)] #[serde(default)]
pub multiple: bool, pub multiple: bool,
pub related: ID, pub related: ID,
#[serde(default)]
pub sort_key: i64,
} }
#[post("/model/relation/create")] #[post("/model/relation/create")]
@ -88,6 +91,7 @@ pub(crate) async fn create(
optional: form.optional, optional: form.optional,
multiple: form.multiple, multiple: form.multiple,
related: form.related, related: form.related,
sort_key: form.sort_key,
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
@ -140,6 +144,8 @@ pub(crate) struct RelationModelEditForm {
pub optional: bool, pub optional: bool,
#[serde(default)] #[serde(default)]
pub multiple: bool, pub multiple: bool,
#[serde(default)]
pub sort_key : i64,
} }
#[get("/model/relation/update/{model_id}")] #[get("/model/relation/update/{model_id}")]
@ -189,6 +195,7 @@ pub(crate) async fn update(
optional: form.optional, optional: form.optional,
multiple: form.multiple, multiple: form.multiple,
related: Default::default(), // dummy related: Default::default(), // dummy
sort_key: form.sort_key,
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;

@ -1,5 +1,4 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use actix_session::Session; use actix_session::Session;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@ -7,6 +6,7 @@ use heck::TitleCase;
use itertools::Itertools; use itertools::Itertools;
use json_dotpath::DotPaths; use json_dotpath::DotPaths;
use serde::Serialize; use serde::Serialize;
use indexmap::IndexMap;
use yopa::{data, model, Storage, ID}; use yopa::{data, model, Storage, ID};
@ -37,11 +37,18 @@ pub struct ObjectDisplay<'a> {
pub name: Cow<'a, str>, pub name: Cow<'a, str>,
} }
#[derive(Debug, Clone, Serialize)]
pub struct ObjectCreate<'a> {
pub id: ID,
pub model: ID,
pub name: Cow<'a, str>,
}
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct ObjectCreateData<'a> { pub struct ObjectCreateData<'a> {
pub model_id: ID, pub model_id: ID,
pub schema: Schema<'a>, pub schema: Schema<'a>,
pub objects: Vec<ObjectDisplay<'a>>, pub objects: Vec<ObjectCreate<'a>>,
} }
#[get("/object/create/{model_id}")] #[get("/object/create/{model_id}")]
@ -96,11 +103,12 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result<O
rel_models: relations, rel_models: relations,
prop_models: rg prop_models: rg
.get_property_models_for_parents(prop_object_ids) .get_property_models_for_parents(prop_object_ids)
.sorted_by(PropertyModel::order_refs)
.collect(), .collect(),
}, },
objects: rg objects: rg
.get_objects_of_types(related_ids) .get_objects_of_types(related_ids)
.map(|o| ObjectDisplay { .map(|o| ObjectCreate {
id: o.id, id: o.id,
model: o.model, model: o.model,
name: rg.get_object_name(o), name: rg.get_object_name(o),
@ -115,8 +123,6 @@ pub(crate) async fn create(
store: crate::YopaStoreWrapper, store: crate::YopaStoreWrapper,
session: Session, session: Session,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
warn!("{:?}", form);
let mut wg = store.write().await; let mut wg = store.write().await;
let form = form.into_inner(); let form = form.into_inner();
@ -161,7 +167,7 @@ pub(crate) async fn list_inner(
let models: Vec<_> = rg let models: Vec<_> = rg
.get_object_models() .get_object_models()
.sorted_by_key(|m| &m.name) .sorted_by(ObjectModel::order_refs)
.map(|model| { .map(|model| {
let objects = objects_by_model.remove(&model.id).unwrap_or_default(); let objects = objects_by_model.remove(&model.id).unwrap_or_default();
let mut objects = objects let mut objects = objects
@ -239,7 +245,9 @@ pub(crate) async fn detail(
context.insert("model", model); context.insert("model", model);
context.insert("kind", &rg.get_model_name(object.model)); context.insert("kind", &rg.get_model_name(object.model));
let relations = rg.get_relations_for_object(object_id).collect_vec(); let relations = rg
.get_relations_for_object(object_id)
.collect_vec();
let reci_relations = rg let reci_relations = rg
.get_reciprocal_relations_for_object(object_id) .get_reciprocal_relations_for_object(object_id)
.collect_vec(); .collect_vec();
@ -266,7 +274,7 @@ pub(crate) async fn detail(
}) })
} }
view_object_properties.sort_by_key(|p| &p.model.name); view_object_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
context.insert("properties", &view_object_properties); context.insert("properties", &view_object_properties);
} }
@ -300,7 +308,7 @@ pub(crate) async fn detail(
}) })
} }
view_rel_properties.sort_by_key(|p| &p.model.name); view_rel_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
let related_name = rg.get_object_name(related_obj); let related_name = rg.get_object_name(related_obj);
@ -324,7 +332,7 @@ pub(crate) async fn detail(
}) })
} }
relation_views.sort_by_key(|r| &r.model.name); relation_views.sort_by(|a, b| RelationModel::order_refs(&a.model, &b.model));
context.insert("relations", &relation_views); context.insert("relations", &relation_views);
} }
@ -358,7 +366,7 @@ pub(crate) async fn detail(
}) })
} }
view_rel_properties.sort_by_key(|p| &p.model.name); view_rel_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
let related_name = rg.get_object_name(related_obj); let related_name = rg.get_object_name(related_obj);
@ -383,7 +391,7 @@ pub(crate) async fn detail(
}) })
} }
relation_views.sort_by_key(|r| &r.model.reciprocal_name); relation_views.sort_by(|a, b| RelationModel::reciprocal_order_refs(&a.model, &b.model));
context.insert("reciprocal_relations", &relation_views); context.insert("reciprocal_relations", &relation_views);
} }
@ -396,11 +404,11 @@ struct EnrichedObject<'a> {
id: ID, id: ID,
model: ID, model: ID,
name: Cow<'a, str>, name: Cow<'a, str>,
values: HashMap< values: IndexMap<
String, /* ID but as string so serde will stop exploding */ String, /* ID but as string so serde will stop exploding */
Vec<&'a data::Value>, Vec<&'a data::Value>,
>, >,
relations: HashMap<String /* ID */, Vec<EnrichedRelation<'a>>>, relations: IndexMap<String /* ID */, Vec<EnrichedRelation<'a>>>,
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
@ -409,7 +417,7 @@ struct EnrichedRelation<'a> {
object: ID, object: ID,
model: ID, model: ID,
related: ID, related: ID,
values: HashMap<String /* ID */, Vec<&'a data::Value>>, values: IndexMap<String /* ID */, Vec<&'a data::Value>>,
} }
#[get("/object/update/{id}")] #[get("/object/update/{id}")]
@ -444,8 +452,8 @@ pub(crate) async fn update_form(
let create_data = prepare_object_create_data(&rg, model.id)?; let create_data = prepare_object_create_data(&rg, model.id)?;
let mut value_map = HashMap::new(); let mut value_map = IndexMap::new();
let mut relation_map = HashMap::new(); let mut relation_map = IndexMap::new();
// Some properties may have no values, so we first check what IDs to expect // Some properties may have no values, so we first check what IDs to expect
let prop_ids = create_data let prop_ids = create_data
@ -453,6 +461,7 @@ pub(crate) async fn update_form(
.prop_models .prop_models
.iter() .iter()
.filter(|p| p.object == model.id) .filter(|p| p.object == model.id)
.sorted_by(PropertyModel::order_refs2)
.map(|p| p.id) .map(|p| p.id)
.collect_vec(); .collect_vec();
@ -500,7 +509,7 @@ pub(crate) async fn update_form(
.unwrap_or_default(); .unwrap_or_default();
for rel in relations { for rel in relations {
let mut relation_values_map = HashMap::new(); let mut relation_values_map = IndexMap::new();
// values keyed by model // values keyed by model
let mut rel_values = relation_values_grouped_by_instance let mut rel_values = relation_values_grouped_by_instance

@ -45,7 +45,7 @@ mod tests;
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const YOPA_MAGIC: &[u8; 4] = b"YOPA"; pub const YOPA_MAGIC: &[u8; 4] = b"YOPA";
pub const BINARY_FORMAT: u8 = 1; pub const BINARY_FORMAT: u16 = 2;
/// Stupid storage with naive inefficient file persistence /// Stupid storage with naive inefficient file persistence
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@ -106,7 +106,7 @@ pub enum StorageError {
#[error("Bad magic! Not a binary Yopa file")] #[error("Bad magic! Not a binary Yopa file")]
BadMagic, BadMagic,
#[error("Binary format {0} is not compatible with this version of Yopa")] #[error("Binary format {0} is not compatible with this version of Yopa")]
NotCompatible(u8), NotCompatible(u16),
#[error(transparent)] #[error(transparent)]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
@ -184,14 +184,14 @@ impl Storage {
let parsed: Self = match self.opts.file_format { let parsed: Self = match self.opts.file_format {
FileEncoding::JSON => serde_json::from_reader(reader)?, FileEncoding::JSON => serde_json::from_reader(reader)?,
FileEncoding::BINCODE => { FileEncoding::BINCODE => {
let mut magic: [u8; 5] = [0; 5]; let mut magic: [u8; 6] = [0; 6];
reader.read_exact(&mut magic)?; reader.read_exact(&mut magic)?;
if &magic[0..4] != YOPA_MAGIC { if &magic[0..4] != YOPA_MAGIC {
return Err(StorageError::BadMagic); return Err(StorageError::BadMagic);
} }
let version = magic[4]; let version = u16::from_le_bytes([magic[4], magic[5]]);
if version != BINARY_FORMAT { if version != BINARY_FORMAT {
return Err(StorageError::NotCompatible(version)); return Err(StorageError::NotCompatible(version));
} }
@ -233,7 +233,7 @@ impl Storage {
} }
FileEncoding::BINCODE => { FileEncoding::BINCODE => {
writer.write_all(YOPA_MAGIC)?; writer.write_all(YOPA_MAGIC)?;
writer.write_all(&[BINARY_FORMAT])?; writer.write_all(&BINARY_FORMAT.to_le_bytes())?;
bincode::serialize_into(writer, self)? bincode::serialize_into(writer, self)?
} }
}; };

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use super::data::TypedValue; use super::data::TypedValue;
use super::ID; use super::ID;
use crate::id::HaveId; use crate::id::HaveId;
use std::cmp::Ordering;
/// Get a description of a struct /// Get a description of a struct
pub trait Describe { pub trait Describe {
@ -26,6 +27,8 @@ pub struct ObjectModel {
/// Property to use as the name in relation selectors /// Property to use as the name in relation selectors
#[serde(default)] #[serde(default)]
pub name_property: Option<ID>, pub name_property: Option<ID>,
/// Sort key, smaller go first
pub sort_key : i64,
} }
/// Relation between templates /// Relation between templates
@ -46,10 +49,12 @@ pub struct RelationModel {
pub multiple: bool, pub multiple: bool,
/// Related object template ID /// Related object template ID
pub related: ID, pub related: ID,
/// Sort key, smaller go first
pub sort_key : i64,
} }
/// Property definition /// Property definition
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PropertyModel { pub struct PropertyModel {
/// PK /// PK
#[serde(default)] #[serde(default)]
@ -68,6 +73,18 @@ pub struct PropertyModel {
pub data_type: DataType, pub data_type: DataType,
/// Default value, used for newly created objects /// Default value, used for newly created objects
pub default: TypedValue, pub default: TypedValue,
/// Sort key, smaller go first
pub sort_key : i64,
/// Additional presentational and data type specific config
pub options : PropertyOptions,
}
/// Additional presentational and data type specific config that shouldn't affect queries and such
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct PropertyOptions {
/// String should be shown as multi-line
pub multiline : bool,
} }
/// Value data type /// Value data type
@ -118,3 +135,86 @@ impl HaveId for PropertyModel {
self.id self.id
} }
} }
// TODO find some less shitty way to do sorting
impl PropertyModel {
/// Get sort
pub fn order(one : &Self, another: &Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// stupid intensifies
pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// hello
pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
}
impl ObjectModel {
/// Get sort
pub fn order(one : &Self, another: &Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// stupid intensifies
pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// hello
pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
}
impl RelationModel {
/// Get sort
pub fn order(one : &Self, another: &Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// stupid intensifies
pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// hello
pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.name.cmp(&another.name))
}
// more stupid
pub fn reciprocal_order(one : &Self, another: &Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
}
pub fn reciprocal_order_refs(one : &&Self, another: &&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
}
pub fn reciprocal_order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
one.sort_key.cmp(&another.sort_key)
.then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
}
}

Loading…
Cancel
Save