Browse Source

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

Ondřej Hruška 3 months ago
parent
commit
f240e85c20
Signed by: Ondřej Hruška <ondra@ondrovo.com> GPG key ID: 2C5FD5035250423D

+ 2 - 0
Cargo.lock View File

@@ -1233,6 +1233,7 @@ checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b"
1233 1233
 dependencies = [
1234 1234
  "autocfg",
1235 1235
  "hashbrown",
1236
+ "serde",
1236 1237
 ]
1237 1238
 
1238 1239
 [[package]]
@@ -2732,6 +2733,7 @@ dependencies = [
2732 2733
  "clap",
2733 2734
  "heck",
2734 2735
  "include_dir",
2736
+ "indexmap",
2735 2737
  "itertools",
2736 2738
  "json_dotpath",
2737 2739
  "log",

+ 1 - 0
yopa-web/Cargo.toml View File

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

+ 6 - 7
yopa-web/resources/src/components/EditObjectForm.vue View File

@@ -14,9 +14,6 @@ export default {
14 14
     let properties = this.schema.prop_models.filter((m) => m.object === model.id);
15 15
     let relations = this.schema.rel_models.filter((m) => m.object === model.id);
16 16
 
17
-    properties.sort((a, b) => a.name.localeCompare(b.name));
18
-    relations.sort((a, b) => a.name.localeCompare(b.name));
19
-
20 17
     let values = {};
21 18
     properties.forEach((p) => {
22 19
       let existing = object.values[p.id] || [];
@@ -38,8 +35,8 @@ export default {
38 35
       }
39 36
     });
40 37
 
41
-    properties = keyBy(properties, 'id');
42
-    relations = keyBy(relations, 'id');
38
+    let propertiesById = keyBy(properties, 'id');
39
+    let relationsById = keyBy(relations, 'id');
43 40
 
44 41
     let model_names = {};
45 42
     this.schema.obj_models.forEach((m) => {
@@ -49,7 +46,9 @@ export default {
49 46
     return {
50 47
       model,
51 48
       properties,
49
+      propertiesById,
52 50
       relations,
51
+      relationsById,
53 52
       haveRelations: !isEmpty(relations),
54 53
       model_names,
55 54
       values,
@@ -67,7 +66,7 @@ export default {
67 66
       let values = [];
68 67
       forEach(objCopy(this.values), (vv, prop_model_id) => {
69 68
         for (let v of vv) {
70
-          if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) {
69
+          if (isEqual(v.value, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
71 70
             continue;
72 71
           }
73 72
 
@@ -148,7 +147,7 @@ export default {
148 147
   </div>
149 148
 
150 149
   <div class="form-horizontal container">
151
-    <edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property>
150
+    <edit-property v-for="property in properties" :model="property" :values="values[property.id]" :key="property.id"></edit-property>
152 151
   </div>
153 152
 
154 153
   <div v-if="haveRelations">

+ 7 - 10
yopa-web/resources/src/components/EditRelationForm.vue View File

@@ -12,12 +12,9 @@ export default {
12 12
 
13 13
     let properties = this.schema.prop_models.filter((m) => m.object === model.id);
14 14
 
15
-    properties.sort((a, b) => a.name.localeCompare(b.name));
16
-
17
-    if (isEmpty(properties)) {
18
-      properties = [];
19
-    } else {
20
-      properties = keyBy(properties, 'id');
15
+    let propertiesById = {};
16
+    if (!isEmpty(properties)) {
17
+      propertiesById = keyBy(properties, 'id');
21 18
     }
22 19
 
23 20
     let related_model = this.schema.obj_models.find((m) => m.id === model.related);
@@ -35,6 +32,7 @@ export default {
35 32
       model,
36 33
       related_model,
37 34
       properties,
35
+      propertiesById,
38 36
       object_names: choices,
39 37
       instances: objCopy(this.initialInstances),
40 38
     }
@@ -56,7 +54,7 @@ export default {
56 54
         let values = [];
57 55
         forEach(instance.values, (vv, prop_model_id) => {
58 56
           for (let v of vv) {
59
-            if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) {
57
+            if (isEqual(v.value, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
60 58
               continue;
61 59
             }
62 60
             v.model = castId(prop_model_id);
@@ -68,7 +66,6 @@ export default {
68 66
         instance.values = values;
69 67
         relations.push(instance);
70 68
       })
71
-      console.log('collected', relations);
72 69
       return relations;
73 70
     },
74 71
 
@@ -112,9 +109,9 @@ export default {
112 109
       </div>
113 110
     </div>
114 111
 
115
-    <edit-property v-for="(property, id) in properties"
112
+    <edit-property v-for="property in properties"
116 113
                    :model="property"
117
-                   :values="instance.values[id]" :key="id"></edit-property>
114
+                   :values="instance.values[property.id]" :key="property.id"></edit-property>
118 115
   </div>
119 116
 
120 117
   <div class="mt-2 mb-2">

+ 4 - 5
yopa-web/resources/src/components/NewObjectForm.vue View File

@@ -12,9 +12,6 @@ export default {
12 12
     let properties = this.schema.prop_models.filter((m) => m.object === model.id);
13 13
     let relations = this.schema.rel_models.filter((m) => m.object === model.id);
14 14
 
15
-    properties.sort((a, b) => a.name.localeCompare(b.name));
16
-    relations.sort((a, b) => a.name.localeCompare(b.name));
17
-
18 15
     let values = {};
19 16
     properties.forEach((p) => {
20 17
       if (p.optional) {
@@ -24,8 +21,8 @@ export default {
24 21
       }
25 22
     });
26 23
 
27
-    properties = keyBy(properties, 'id');
28
-    relations = keyBy(relations, 'id');
24
+    let propertiesById = keyBy(properties, 'id');
25
+    let relationsById = keyBy(relations, 'id');
29 26
 
30 27
     let model_names = {};
31 28
     this.schema.obj_models.forEach((m) => {
@@ -36,6 +33,8 @@ export default {
36 33
       model,
37 34
       properties,
38 35
       relations,
36
+      propertiesById,
37
+      relationsById,
39 38
       haveRelations: !isEmpty(relations),
40 39
       model_names,
41 40
       values,

+ 7 - 9
yopa-web/resources/src/components/NewRelationForm.vue View File

@@ -12,12 +12,9 @@ export default {
12 12
 
13 13
     let properties = this.schema.prop_models.filter((m) => m.object === model.id);
14 14
 
15
-    properties.sort((a, b) => a.name.localeCompare(b.name));
16
-
17
-    if (isEmpty(properties)) {
18
-      properties = [];
19
-    } else {
20
-      properties = keyBy(properties, 'id');
15
+    let propertiesById = {};
16
+    if (!isEmpty(properties)) {
17
+      propertiesById = keyBy(properties, 'id');
21 18
     }
22 19
 
23 20
     let related_model = this.schema.obj_models.find((m) => m.id === model.related);
@@ -52,6 +49,7 @@ export default {
52 49
       model,
53 50
       related_model,
54 51
       properties,
52
+      propertiesById,
55 53
       object_names: choices,
56 54
       instances,
57 55
     }
@@ -71,7 +69,7 @@ export default {
71 69
         forEach(instance.values, (vv, prop_model_id) => {
72 70
 
73 71
           for (let v of vv) {
74
-            if (isEqual(v, {"String": ""}) && this.properties[prop_model_id].optional) {
72
+            if (isEqual(v, {"String": ""}) && this.propertiesById[prop_model_id].optional) {
75 73
               continue;
76 74
             }
77 75
 
@@ -131,9 +129,9 @@ export default {
131 129
       </div>
132 130
     </div>
133 131
 
134
-    <property v-for="(property, id) in properties"
132
+    <property v-for="property in properties"
135 133
                    :model="property"
136
-                   :values="instance.values[id]" :key="id"></property>
134
+                   :values="instance.values[property.id]" :key="property.id"></property>
137 135
   </div>
138 136
 
139 137
   <div class="mt-2 mb-2">

+ 24 - 0
yopa-web/resources/src/main.js View File

@@ -6,6 +6,7 @@ import StringValue from "./components/value/StringValue.vue";
6 6
 import DecimalValue from "./components/value/DecimalValue.vue";
7 7
 import BooleanValue from "./components/value/BooleanValue.vue";
8 8
 import IntegerValue from "./components/value/IntegerValue.vue";
9
+import TextValue from "./components/value/TextValue.vue";
9 10
 import PropertyField from "./components/PropertyField.vue";
10 11
 
11 12
 import NewObjectForm from "./components/NewObjectForm.vue";
@@ -14,9 +15,11 @@ import NewRelationForm from "./components/NewRelationForm.vue"
14 15
 import EditObjectForm from "./components/EditObjectForm.vue";
15 16
 import EditRelationForm from "./components/EditRelationForm.vue";
16 17
 import EditPropertyField from "./components/EditPropertyField.vue";
18
+import {qs} from "./utils";
17 19
 
18 20
 function registerComponents(app) {
19 21
   app.component('string-value', StringValue);
22
+  app.component('text-value', StringValue);
20 23
   app.component('integer-value', IntegerValue);
21 24
   app.component('decimal-value', DecimalValue);
22 25
   app.component('boolean-value', BooleanValue);
@@ -48,6 +51,27 @@ window.Yopa = {
48 51
 
49 52
     // ...
50 53
     return instance;
54
+  },
55
+  propertyEditForm() {
56
+    // multiple and unique are XORed. This is also enforced server-side
57
+    let multiple = qs('#multiple');
58
+    let unique = qs('#unique');
59
+    let type = qs('#data_type');
60
+    unique.addEventListener('input', function () {
61
+      multiple.checked &= !unique.checked;
62
+    })
63
+    multiple.addEventListener('input', function () {
64
+      unique.checked &= !multiple.checked;
65
+    })
66
+
67
+    type.addEventListener('input', function () {
68
+      console.log(type.value);
69
+      toggleOptionalBoxes();
70
+    })
71
+    function toggleOptionalBoxes() {
72
+      qs('#string-options').classList.toggle('hidden', type.value !== 'String');
73
+    }
74
+    toggleOptionalBoxes();
51 75
   }
52 76
 };
53 77
 

+ 4 - 0
yopa-web/resources/src/style/app.scss View File

@@ -17,3 +17,7 @@ table.object-display {
17 17
    width: 225px;
18 18
   }
19 19
 }
20
+
21
+.hidden {
22
+  display: none !important;
23
+}

+ 3 - 0
yopa-web/resources/src/utils.js View File

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

File diff suppressed because it is too large
+ 1 - 1
yopa-web/resources/static/bundle.js


File diff suppressed because it is too large
+ 1 - 1
yopa-web/resources/static/bundle.js.map


+ 3 - 0
yopa-web/resources/static/style.css View File

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

+ 11 - 0
yopa-web/resources/templates/_form_macros.html.tera View File

@@ -24,6 +24,17 @@
24 24
 	</div>
25 25
 {% endmacro input %}
26 26
 
27
+{% macro integer(name, label, value) %}
28
+<div class="form-group cols">
29
+	<div class="col-3 pl-2">
30
+		<label class="form-label" for="{{name}}">{{label}}</label>
31
+	</div>
32
+	<div class="col-9 pr-2">
33
+		<input type="number" step="1" class="form-input input-inline" id="{{name}}" name="{{name}}" value="{{value}}" autocomplete="off">
34
+	</div>
35
+</div>
36
+{% endmacro input %}
37
+
27 38
 {% macro checkbox(name, label, checked) %}
28 39
 	<div class="form-group cols">
29 40
 		<div class="col-3 pl-2">

+ 2 - 0
yopa-web/resources/templates/models/model_create.html.tera View File

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

+ 2 - 0
yopa-web/resources/templates/models/model_update.html.tera View File

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

+ 7 - 9
yopa-web/resources/templates/models/property_create.html.tera View File

@@ -38,20 +38,18 @@ Define property
38 38
 		</div>
39 39
 
40 40
 		{{ form::text(name="default", label="Default", value=old.default) }}
41
+
42
+		{{ form::integer(name="sort_key", label="Sort order", value=old.sort_key) }}
43
+
44
+		<div class="hidden" id="string-options">
45
+			{{ form::checkbox(name="opt_multiline", label="Multi-line", checked=old.opt_multiline) }}
46
+		</div>
41 47
 	</div>
42 48
 </form>
43 49
 
44 50
 <script>
45 51
   (function () {
46
-    // multiple and unique are XORed. This is also enforced server-side
47
-  	let multiple = document.getElementById('multiple');
48
-  	let unique = document.getElementById('unique');
49
-    unique.addEventListener('input', function () {
50
-      multiple.checked &= !unique.checked;
51
-	})
52
-    multiple.addEventListener('input', function () {
53
-      unique.checked &= !multiple.checked;
54
-    })
52
+    Yopa.propertyEditForm()
55 53
   })();
56 54
 </script>
57 55
 

+ 7 - 9
yopa-web/resources/templates/models/property_update.html.tera View File

@@ -36,20 +36,18 @@ Edit property
36 36
 		</div>
37 37
 
38 38
 		{{ form::text(name="default", label="Default", value=model.default|print_typed_value) }}
39
+
40
+		{{ form::integer(name="sort_key", label="Sort order", value=model.sort_key) }}
41
+
42
+		<div id="string-options">
43
+			{{ form::checkbox(name="opt_multiline", label="Multi-line", checked=model.options.multiline) }}
44
+		</div>
39 45
 	</div>
40 46
 </form>
41 47
 
42 48
 <script>
43 49
   (function () {
44
-    // multiple and unique are XORed. This is also enforced server-side
45
-  	let multiple = document.getElementById('multiple');
46
-  	let unique = document.getElementById('unique');
47
-    unique.addEventListener('input', function () {
48
-      multiple.checked &= !unique.checked;
49
-	})
50
-    multiple.addEventListener('input', function () {
51
-      unique.checked &= !multiple.checked;
52
-    })
50
+    Yopa.propertyEditForm()
53 51
   })();
54 52
 </script>
55 53
 

+ 1 - 0
yopa-web/resources/templates/models/relation_create.html.tera View File

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

+ 1 - 0
yopa-web/resources/templates/models/relation_update.html.tera View File

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

+ 2 - 0
yopa-web/src/main.rs View File

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

+ 8 - 6
yopa-web/src/routes/models.rs View File

@@ -5,6 +5,7 @@ use crate::tera_ext::TeraExt;
5 5
 use crate::TERA;
6 6
 use actix_session::Session;
7 7
 use actix_web::Responder;
8
+use yopa::model::{PropertyModel, RelationModel, ObjectModel};
8 9
 
9 10
 pub(crate) mod object;
10 11
 pub(crate) mod property;
@@ -32,7 +33,7 @@ pub(crate) async fn list(
32 33
             .into_iter()
33 34
             .map(|rm| {
34 35
                 let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
35
-                rprops.sort_by_key(|m| &m.name);
36
+                rprops.sort_by(PropertyModel::order_refs);
36 37
 
37 38
                 RelationModelDisplay {
38 39
                     model: rm,
@@ -41,7 +42,7 @@ pub(crate) async fn list(
41 42
                 }
42 43
             })
43 44
             .collect::<Vec<_>>();
44
-        relations.sort_by_key(|d| &d.model.name);
45
+        relations.sort_by(|a, b| RelationModel::order_refs(&a.model, &b.model));
45 46
 
46 47
         // Relations coming INTO this model
47 48
         let reciprocal_relations = model_rec_relations.remove(&om.id).unwrap_or_default();
@@ -49,7 +50,7 @@ pub(crate) async fn list(
49 50
             .into_iter()
50 51
             .map(|rm| {
51 52
                 let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
52
-                rprops.sort_by_key(|m| &m.name);
53
+                rprops.sort_by(PropertyModel::order_refs);
53 54
 
54 55
                 RelationModelDisplay {
55 56
                     model: rm,
@@ -58,21 +59,22 @@ pub(crate) async fn list(
58 59
                 }
59 60
             })
60 61
             .collect::<Vec<_>>();
61
-        reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name);
62
+        reciprocal_relations.sort_by(|a, b| RelationModel::reciprocal_order_refs(&a.model, &b.model));
62 63
 
63 64
         let mut properties = model_props.remove(&om.id).unwrap_or_default();
64
-        properties.sort_by_key(|m| &m.name);
65
+        properties.sort_by(PropertyModel::order_refs);
65 66
 
66 67
         models.push(ObjectModelDisplay {
67 68
             id: om.id,
68 69
             name: &om.name,
70
+            model: &om,
69 71
             properties,
70 72
             relations,
71 73
             reciprocal_relations,
72 74
         })
73 75
     }
74 76
 
75
-    models.sort_by_key(|m| m.name);
77
+    models.sort_by(|a, b| ObjectModel::order_refs(&a.model, &b.model));
76 78
 
77 79
     let mut ctx = tera::Context::new();
78 80
     ctx.insert("models", &models);

+ 7 - 0
yopa-web/src/routes/models/object.rs View File

@@ -16,6 +16,8 @@ use itertools::Itertools;
16 16
 pub(crate) struct ObjectModelDisplay<'a> {
17 17
     pub(crate) id: yopa::ID,
18 18
     pub(crate) name: &'a str,
19
+    #[serde(skip)]
20
+    pub(crate) model : &'a ObjectModel,
19 21
     pub(crate) properties: Vec<&'a PropertyModel>,
20 22
     pub(crate) relations: Vec<RelationModelDisplay<'a>>,
21 23
     pub(crate) reciprocal_relations: Vec<RelationModelDisplay<'a>>,
@@ -43,6 +45,8 @@ pub(crate) struct ObjectModelForm {
43 45
     // #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected
44 46
     #[serde(with = "my_string_empty_as_none")]
45 47
     pub name_property: Option<ID>,
48
+    #[serde(default)]
49
+    pub sort_key: i64,
46 50
 }
47 51
 
48 52
 #[post("/model/object/create")]
@@ -57,6 +61,7 @@ pub(crate) async fn create(
57 61
         id: Default::default(),
58 62
         name: form.name.clone(),
59 63
         name_property: form.name_property,
64
+        sort_key: form.sort_key
60 65
     }) {
61 66
         Ok(_id) => {
62 67
             wg.persist().err_to_500()?;
@@ -101,6 +106,7 @@ pub(crate) async fn update_form(
101 106
             &ObjectModelForm {
102 107
                 name: model.name.to_string(),
103 108
                 name_property: model.name_property,
109
+                sort_key: model.sort_key,
104 110
             },
105 111
         );
106 112
     }
@@ -127,6 +133,7 @@ pub(crate) async fn update(
127 133
         id,
128 134
         name: form.name.clone(),
129 135
         name_property: form.name_property,
136
+        sort_key: form.sort_key,
130 137
     }) {
131 138
         Ok(_id) => {
132 139
             wg.persist().err_to_500()?;

+ 22 - 2
yopa-web/src/routes/models/property.rs View File

@@ -2,7 +2,7 @@ use actix_session::Session;
2 2
 use actix_web::{web, Responder};
3 3
 use serde::{Deserialize, Serialize};
4 4
 
5
-use yopa::model::PropertyModel;
5
+use yopa::model::{PropertyModel, PropertyOptions};
6 6
 use yopa::{DataType, TypedValue, ID};
7 7
 
8 8
 use crate::routes::models::relation::ObjectOrRelationModelDisplay;
@@ -37,6 +37,8 @@ pub(crate) async fn create_form(
37 37
                 unique: false,
38 38
                 data_type: DataType::String,
39 39
                 default: "".to_string(),
40
+                sort_key: 1000, // big number so it goes at the end by default
41
+                opt_multiline: false,
40 42
             },
41 43
         );
42 44
     }
@@ -78,6 +80,10 @@ pub(crate) struct PropertyModelCreateForm {
78 80
     /// Default value to be parsed to the data type
79 81
     /// May be unused if empty and optional
80 82
     pub default: String,
83
+    #[serde(default)]
84
+    pub sort_key: i64,
85
+    #[serde(default)]
86
+    pub opt_multiline: bool,
81 87
 }
82 88
 
83 89
 fn parse_default(data_type: DataType, default: String) -> Result<TypedValue, String> {
@@ -151,6 +157,10 @@ pub(crate) async fn create(
151 157
         unique,
152 158
         data_type: form.data_type,
153 159
         default,
160
+        sort_key: form.sort_key,
161
+        options: PropertyOptions {
162
+            multiline: form.opt_multiline
163
+        }
154 164
     }) {
155 165
         Ok(_id) => {
156 166
             wg.persist().err_to_500()?;
@@ -202,6 +212,10 @@ pub(crate) struct PropertyModelEditForm {
202 212
     /// Default value to be parsed to the data type
203 213
     /// May be unused if empty and optional
204 214
     pub default: String,
215
+    #[serde(default)]
216
+    pub sort_key: i64,
217
+    #[serde(default)]
218
+    pub opt_multiline: bool,
205 219
 }
206 220
 
207 221
 #[get("/model/property/update/{model_id}")]
@@ -227,9 +241,11 @@ pub(crate) async fn update_form(
227 241
         model.optional = form.optional;
228 242
         model.multiple = form.multiple;
229 243
         model.unique = form.unique;
244
+        model.sort_key = form.sort_key;
245
+        model.options.multiline = form.opt_multiline;
230 246
         context.insert("model", &model);
231 247
     } else {
232
-        context.insert("model", model);
248
+        context.insert("model", &model);
233 249
     }
234 250
 
235 251
     TERA.build_response("models/property_update", &context)
@@ -265,6 +281,10 @@ pub(crate) async fn update(
265 281
         unique: form.unique,
266 282
         data_type: form.data_type,
267 283
         default,
284
+        sort_key: form.sort_key,
285
+        options: PropertyOptions {
286
+            multiline: form.opt_multiline
287
+        }
268 288
     }) {
269 289
         Ok(_id) => {
270 290
             wg.persist().err_to_500()?;

+ 7 - 0
yopa-web/src/routes/models/relation.rs View File

@@ -42,6 +42,7 @@ pub(crate) async fn create_form(
42 42
                 optional: false,
43 43
                 multiple: false,
44 44
                 related: Default::default(),
45
+                sort_key: 1000
45 46
             },
46 47
         );
47 48
     }
@@ -70,6 +71,8 @@ pub(crate) struct RelationModelCreateForm {
70 71
     #[serde(default)]
71 72
     pub multiple: bool,
72 73
     pub related: ID,
74
+    #[serde(default)]
75
+    pub sort_key: i64,
73 76
 }
74 77
 
75 78
 #[post("/model/relation/create")]
@@ -88,6 +91,7 @@ pub(crate) async fn create(
88 91
         optional: form.optional,
89 92
         multiple: form.multiple,
90 93
         related: form.related,
94
+        sort_key: form.sort_key,
91 95
     }) {
92 96
         Ok(_id) => {
93 97
             wg.persist().err_to_500()?;
@@ -140,6 +144,8 @@ pub(crate) struct RelationModelEditForm {
140 144
     pub optional: bool,
141 145
     #[serde(default)]
142 146
     pub multiple: bool,
147
+    #[serde(default)]
148
+    pub sort_key : i64,
143 149
 }
144 150
 
145 151
 #[get("/model/relation/update/{model_id}")]
@@ -189,6 +195,7 @@ pub(crate) async fn update(
189 195
         optional: form.optional,
190 196
         multiple: form.multiple,
191 197
         related: Default::default(), // dummy
198
+        sort_key: form.sort_key,
192 199
     }) {
193 200
         Ok(_id) => {
194 201
             wg.persist().err_to_500()?;

+ 27 - 18
yopa-web/src/routes/objects.rs View File

@@ -1,5 +1,4 @@
1 1
 use std::borrow::Cow;
2
-use std::collections::HashMap;
3 2
 
4 3
 use actix_session::Session;
5 4
 use actix_web::{web, HttpResponse, Responder};
@@ -7,6 +6,7 @@ use heck::TitleCase;
7 6
 use itertools::Itertools;
8 7
 use json_dotpath::DotPaths;
9 8
 use serde::Serialize;
9
+use indexmap::IndexMap;
10 10
 
11 11
 use yopa::{data, model, Storage, ID};
12 12
 
@@ -37,11 +37,18 @@ pub struct ObjectDisplay<'a> {
37 37
     pub name: Cow<'a, str>,
38 38
 }
39 39
 
40
+#[derive(Debug, Clone, Serialize)]
41
+pub struct ObjectCreate<'a> {
42
+    pub id: ID,
43
+    pub model: ID,
44
+    pub name: Cow<'a, str>,
45
+}
46
+
40 47
 #[derive(Serialize, Debug, Clone)]
41 48
 pub struct ObjectCreateData<'a> {
42 49
     pub model_id: ID,
43 50
     pub schema: Schema<'a>,
44
-    pub objects: Vec<ObjectDisplay<'a>>,
51
+    pub objects: Vec<ObjectCreate<'a>>,
45 52
 }
46 53
 
47 54
 #[get("/object/create/{model_id}")]
@@ -96,11 +103,12 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result<O
96 103
             rel_models: relations,
97 104
             prop_models: rg
98 105
                 .get_property_models_for_parents(prop_object_ids)
106
+                .sorted_by(PropertyModel::order_refs)
99 107
                 .collect(),
100 108
         },
101 109
         objects: rg
102 110
             .get_objects_of_types(related_ids)
103
-            .map(|o| ObjectDisplay {
111
+            .map(|o| ObjectCreate {
104 112
                 id: o.id,
105 113
                 model: o.model,
106 114
                 name: rg.get_object_name(o),
@@ -115,8 +123,6 @@ pub(crate) async fn create(
115 123
     store: crate::YopaStoreWrapper,
116 124
     session: Session,
117 125
 ) -> actix_web::Result<impl Responder> {
118
-    warn!("{:?}", form);
119
-
120 126
     let mut wg = store.write().await;
121 127
     let form = form.into_inner();
122 128
 
@@ -161,7 +167,7 @@ pub(crate) async fn list_inner(
161 167
 
162 168
     let models: Vec<_> = rg
163 169
         .get_object_models()
164
-        .sorted_by_key(|m| &m.name)
170
+        .sorted_by(ObjectModel::order_refs)
165 171
         .map(|model| {
166 172
             let objects = objects_by_model.remove(&model.id).unwrap_or_default();
167 173
             let mut objects = objects
@@ -239,7 +245,9 @@ pub(crate) async fn detail(
239 245
     context.insert("model", model);
240 246
     context.insert("kind", &rg.get_model_name(object.model));
241 247
 
242
-    let relations = rg.get_relations_for_object(object_id).collect_vec();
248
+    let relations = rg
249
+        .get_relations_for_object(object_id)
250
+        .collect_vec();
243 251
     let reci_relations = rg
244 252
         .get_reciprocal_relations_for_object(object_id)
245 253
         .collect_vec();
@@ -266,7 +274,7 @@ pub(crate) async fn detail(
266 274
             })
267 275
         }
268 276
 
269
-        view_object_properties.sort_by_key(|p| &p.model.name);
277
+        view_object_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
270 278
 
271 279
         context.insert("properties", &view_object_properties);
272 280
     }
@@ -300,7 +308,7 @@ pub(crate) async fn detail(
300 308
                     })
301 309
                 }
302 310
 
303
-                view_rel_properties.sort_by_key(|p| &p.model.name);
311
+                view_rel_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
304 312
 
305 313
                 let related_name = rg.get_object_name(related_obj);
306 314
 
@@ -324,7 +332,7 @@ pub(crate) async fn detail(
324 332
             })
325 333
         }
326 334
 
327
-        relation_views.sort_by_key(|r| &r.model.name);
335
+        relation_views.sort_by(|a, b| RelationModel::order_refs(&a.model, &b.model));
328 336
 
329 337
         context.insert("relations", &relation_views);
330 338
     }
@@ -358,7 +366,7 @@ pub(crate) async fn detail(
358 366
                     })
359 367
                 }
360 368
 
361
-                view_rel_properties.sort_by_key(|p| &p.model.name);
369
+                view_rel_properties.sort_by(|a, b| PropertyModel::order(a.model, b.model));
362 370
 
363 371
                 let related_name = rg.get_object_name(related_obj);
364 372
 
@@ -383,7 +391,7 @@ pub(crate) async fn detail(
383 391
             })
384 392
         }
385 393
 
386
-        relation_views.sort_by_key(|r| &r.model.reciprocal_name);
394
+        relation_views.sort_by(|a, b| RelationModel::reciprocal_order_refs(&a.model, &b.model));
387 395
 
388 396
         context.insert("reciprocal_relations", &relation_views);
389 397
     }
@@ -396,11 +404,11 @@ struct EnrichedObject<'a> {
396 404
     id: ID,
397 405
     model: ID,
398 406
     name: Cow<'a, str>,
399
-    values: HashMap<
407
+    values: IndexMap<
400 408
         String, /* ID but as string so serde will stop exploding */
401 409
         Vec<&'a data::Value>,
402 410
     >,
403
-    relations: HashMap<String /* ID */, Vec<EnrichedRelation<'a>>>,
411
+    relations: IndexMap<String /* ID */, Vec<EnrichedRelation<'a>>>,
404 412
 }
405 413
 
406 414
 #[derive(Serialize, Debug, Clone)]
@@ -409,7 +417,7 @@ struct EnrichedRelation<'a> {
409 417
     object: ID,
410 418
     model: ID,
411 419
     related: ID,
412
-    values: HashMap<String /* ID */, Vec<&'a data::Value>>,
420
+    values: IndexMap<String /* ID */, Vec<&'a data::Value>>,
413 421
 }
414 422
 
415 423
 #[get("/object/update/{id}")]
@@ -444,8 +452,8 @@ pub(crate) async fn update_form(
444 452
 
445 453
     let create_data = prepare_object_create_data(&rg, model.id)?;
446 454
 
447
-    let mut value_map = HashMap::new();
448
-    let mut relation_map = HashMap::new();
455
+    let mut value_map = IndexMap::new();
456
+    let mut relation_map = IndexMap::new();
449 457
 
450 458
     // Some properties may have no values, so we first check what IDs to expect
451 459
     let prop_ids = create_data
@@ -453,6 +461,7 @@ pub(crate) async fn update_form(
453 461
         .prop_models
454 462
         .iter()
455 463
         .filter(|p| p.object == model.id)
464
+        .sorted_by(PropertyModel::order_refs2)
456 465
         .map(|p| p.id)
457 466
         .collect_vec();
458 467
 
@@ -500,7 +509,7 @@ pub(crate) async fn update_form(
500 509
                 .unwrap_or_default();
501 510
 
502 511
             for rel in relations {
503
-                let mut relation_values_map = HashMap::new();
512
+                let mut relation_values_map = IndexMap::new();
504 513
 
505 514
                 // values keyed by model
506 515
                 let mut rel_values = relation_values_grouped_by_instance

+ 5 - 5
yopa/src/lib.rs View File

@@ -45,7 +45,7 @@ mod tests;
45 45
 pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
46 46
 
47 47
 pub const YOPA_MAGIC: &[u8; 4] = b"YOPA";
48
-pub const BINARY_FORMAT: u8 = 1;
48
+pub const BINARY_FORMAT: u16 = 2;
49 49
 
50 50
 /// Stupid storage with naive inefficient file persistence
51 51
 #[derive(Debug, Default, Serialize, Deserialize)]
@@ -106,7 +106,7 @@ pub enum StorageError {
106 106
     #[error("Bad magic! Not a binary Yopa file")]
107 107
     BadMagic,
108 108
     #[error("Binary format {0} is not compatible with this version of Yopa")]
109
-    NotCompatible(u8),
109
+    NotCompatible(u16),
110 110
     #[error(transparent)]
111 111
     IO(#[from] std::io::Error),
112 112
     #[error(transparent)]
@@ -184,14 +184,14 @@ impl Storage {
184 184
                 let parsed: Self = match self.opts.file_format {
185 185
                     FileEncoding::JSON => serde_json::from_reader(reader)?,
186 186
                     FileEncoding::BINCODE => {
187
-                        let mut magic: [u8; 5] = [0; 5];
187
+                        let mut magic: [u8; 6] = [0; 6];
188 188
                         reader.read_exact(&mut magic)?;
189 189
 
190 190
                         if &magic[0..4] != YOPA_MAGIC {
191 191
                             return Err(StorageError::BadMagic);
192 192
                         }
193 193
 
194
-                        let version = magic[4];
194
+                        let version = u16::from_le_bytes([magic[4], magic[5]]);
195 195
                         if version != BINARY_FORMAT {
196 196
                             return Err(StorageError::NotCompatible(version));
197 197
                         }
@@ -233,7 +233,7 @@ impl Storage {
233 233
                     }
234 234
                     FileEncoding::BINCODE => {
235 235
                         writer.write_all(YOPA_MAGIC)?;
236
-                        writer.write_all(&[BINARY_FORMAT])?;
236
+                        writer.write_all(&BINARY_FORMAT.to_le_bytes())?;
237 237
                         bincode::serialize_into(writer, self)?
238 238
                     }
239 239
                 };

+ 101 - 1
yopa/src/model.rs View File

@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
8 8
 use super::data::TypedValue;
9 9
 use super::ID;
10 10
 use crate::id::HaveId;
11
+use std::cmp::Ordering;
11 12
 
12 13
 /// Get a description of a struct
13 14
 pub trait Describe {
@@ -26,6 +27,8 @@ pub struct ObjectModel {
26 27
     /// Property to use as the name in relation selectors
27 28
     #[serde(default)]
28 29
     pub name_property: Option<ID>,
30
+    /// Sort key, smaller go first
31
+    pub sort_key : i64,
29 32
 }
30 33
 
31 34
 /// Relation between templates
@@ -46,10 +49,12 @@ pub struct RelationModel {
46 49
     pub multiple: bool,
47 50
     /// Related object template ID
48 51
     pub related: ID,
52
+    /// Sort key, smaller go first
53
+    pub sort_key : i64,
49 54
 }
50 55
 
51 56
 /// Property definition
52
-#[derive(Debug, Clone, Serialize, Deserialize)]
57
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53 58
 pub struct PropertyModel {
54 59
     /// PK
55 60
     #[serde(default)]
@@ -68,6 +73,18 @@ pub struct PropertyModel {
68 73
     pub data_type: DataType,
69 74
     /// Default value, used for newly created objects
70 75
     pub default: TypedValue,
76
+    /// Sort key, smaller go first
77
+    pub sort_key : i64,
78
+
79
+    /// Additional presentational and data type specific config
80
+    pub options : PropertyOptions,
81
+}
82
+
83
+/// Additional presentational and data type specific config that shouldn't affect queries and such
84
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
85
+pub struct PropertyOptions {
86
+    /// String should be shown as multi-line
87
+    pub multiline : bool,
71 88
 }
72 89
 
73 90
 /// Value data type
@@ -118,3 +135,86 @@ impl HaveId for PropertyModel {
118 135
         self.id
119 136
     }
120 137
 }
138
+
139
+// TODO find some less shitty way to do sorting
140
+
141
+
142
+impl PropertyModel {
143
+    /// Get sort
144
+    pub fn order(one : &Self, another: &Self) -> Ordering {
145
+        one.sort_key.cmp(&another.sort_key)
146
+            .then_with(|| one.name.cmp(&another.name))
147
+    }
148
+
149
+    // stupid intensifies
150
+    pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
151
+        one.sort_key.cmp(&another.sort_key)
152
+            .then_with(|| one.name.cmp(&another.name))
153
+    }
154
+
155
+    // hello
156
+    pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
157
+        one.sort_key.cmp(&another.sort_key)
158
+            .then_with(|| one.name.cmp(&another.name))
159
+    }
160
+}
161
+
162
+
163
+
164
+impl ObjectModel {
165
+    /// Get sort
166
+    pub fn order(one : &Self, another: &Self) -> Ordering {
167
+        one.sort_key.cmp(&another.sort_key)
168
+            .then_with(|| one.name.cmp(&another.name))
169
+    }
170
+
171
+    // stupid intensifies
172
+    pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
173
+        one.sort_key.cmp(&another.sort_key)
174
+            .then_with(|| one.name.cmp(&another.name))
175
+    }
176
+
177
+    // hello
178
+    pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
179
+        one.sort_key.cmp(&another.sort_key)
180
+            .then_with(|| one.name.cmp(&another.name))
181
+    }
182
+}
183
+
184
+impl RelationModel {
185
+    /// Get sort
186
+    pub fn order(one : &Self, another: &Self) -> Ordering {
187
+        one.sort_key.cmp(&another.sort_key)
188
+            .then_with(|| one.name.cmp(&another.name))
189
+    }
190
+
191
+    // stupid intensifies
192
+    pub fn order_refs(one : &&Self, another: &&Self) -> Ordering {
193
+        one.sort_key.cmp(&another.sort_key)
194
+            .then_with(|| one.name.cmp(&another.name))
195
+    }
196
+
197
+    // hello
198
+    pub fn order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
199
+        one.sort_key.cmp(&another.sort_key)
200
+            .then_with(|| one.name.cmp(&another.name))
201
+    }
202
+
203
+
204
+    // more stupid
205
+
206
+    pub fn reciprocal_order(one : &Self, another: &Self) -> Ordering {
207
+        one.sort_key.cmp(&another.sort_key)
208
+            .then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
209
+    }
210
+
211
+    pub fn reciprocal_order_refs(one : &&Self, another: &&Self) -> Ordering {
212
+        one.sort_key.cmp(&another.sort_key)
213
+            .then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
214
+    }
215
+
216
+    pub fn reciprocal_order_refs2(one : &&&Self, another: &&&Self) -> Ordering {
217
+        one.sort_key.cmp(&another.sort_key)
218
+            .then_with(|| one.reciprocal_name.cmp(&another.reciprocal_name))
219
+    }
220
+}