names are now a property and can be chosen arbitrarily
This commit is contained in:
@@ -27,6 +27,7 @@ json_dotpath = "1.0.3"
|
||||
anyhow = "1.0.38"
|
||||
thiserror = "1.0.24"
|
||||
clap = "2"
|
||||
serde_with = "1.6.4"
|
||||
|
||||
tokio = { version="0.2.6", features=["full"] }
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ export default {
|
||||
return {
|
||||
model: this.object.model, // string is fine
|
||||
id: this.object.id,
|
||||
name: this.name,
|
||||
values,
|
||||
relations,
|
||||
};
|
||||
@@ -140,13 +139,6 @@ export default {
|
||||
<p><input type="button" value="Save" @click="trySave"></p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="field-name">Name</label></th>
|
||||
<td>
|
||||
<input type="text" id="field-name" v-model="name">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -39,17 +39,12 @@ export default {
|
||||
haveRelations: !isEmpty(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, prop_model_id) => {
|
||||
for (let v of vv) {
|
||||
@@ -73,7 +68,6 @@ export default {
|
||||
|
||||
return {
|
||||
model: this.model.id,
|
||||
name: this.name,
|
||||
values,
|
||||
relations,
|
||||
};
|
||||
@@ -127,13 +121,6 @@ export default {
|
||||
<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>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,8 +13,13 @@ Define object
|
||||
<h1>Define new object model</h1>
|
||||
|
||||
<form action="/model/object/create" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -13,8 +13,24 @@ Edit object model
|
||||
<h1>Edit object model {{ model.name }}</h1>
|
||||
|
||||
<form action="/model/object/update/{{ model.id }}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="name_property">Name property:</label></th>
|
||||
<td>
|
||||
<select name="name_property" id="name_property" autocomplete="off">
|
||||
<option value=""></option>
|
||||
{% for p in properties %}
|
||||
<option value="{{ p.id }}" {{selected(val=old.name_property, opt=p.id)}}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -16,28 +16,55 @@ Define property
|
||||
<form action="/model/property/create" method="POST">
|
||||
<input type="hidden" name="object" value="{{object.id}}">
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="data_type">Type:</label>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(opt="String",val=old.data_type)}}>String</option>
|
||||
<option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option>
|
||||
<option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option>
|
||||
</select><br>
|
||||
|
||||
<label for="default">Default:</label>
|
||||
<input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="unique">Unique:</label></th>
|
||||
<td><input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=old.unique)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="data_type">Type:</label></th>
|
||||
<td>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(opt="String",val=old.data_type)}}>String</option>
|
||||
<option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option>
|
||||
<option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="default">Default:</label></th>
|
||||
<td><input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// multiple and unique are XORed. This is also enforced server-side
|
||||
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>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -13,28 +13,64 @@ Edit property
|
||||
<h1>Edit property {{model.name}}</h1>
|
||||
|
||||
<form action="/model/property/update/{{model.id}}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="data_type">Type:</label>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option>
|
||||
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option>
|
||||
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option>
|
||||
</select><br>
|
||||
|
||||
<label for="default">Default:</label>
|
||||
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="unique">Unique:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=model.unique)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="data_type">Type:</label></th>
|
||||
<td>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option>
|
||||
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option>
|
||||
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="default">Default:</label></th>
|
||||
<td>
|
||||
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// multiple and unique are XORed. This is also enforced server-side
|
||||
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>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -15,25 +15,42 @@ Define relation
|
||||
<form action="/model/relation/create" method="POST">
|
||||
<input type="hidden" name="object" value="{{object.id}}">
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="reciprocal_name">Reciprocal name:</label>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="related">Related object:</label>
|
||||
<select name="related" id="related" autocomplete="off">
|
||||
{% for m in models %}
|
||||
<option value="{{ m.id }}" {{selected(val=old.related, opt=m.id)}}>{{ m.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="reciprocal_name">Reciprocal name:</label></th>
|
||||
<td>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="related">Related object:</label></th>
|
||||
<td>
|
||||
<select name="related" id="related" autocomplete="off">
|
||||
{% for m in models %}
|
||||
<option value="{{ m.id }}" {{selected(val=old.related, opt=m.id)}}>{{ m.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -13,18 +13,29 @@ Edit relation
|
||||
<h1>Edit relation model "{{model.name}}"</h1>
|
||||
|
||||
<form action="/model/relation/update/{{model.id}}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="reciprocal_name">Reciprocal name:</label>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="reciprocal_name">Reciprocal name:</label></th>
|
||||
<td><input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label>
|
||||
</th>
|
||||
<td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>The related object cannot be changed. Create a new relation if needed.</p>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::session_ext::SessionExt;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
use crate::TERA;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(crate) struct ObjectModelDisplay<'a> {
|
||||
@@ -38,6 +39,10 @@ pub(crate) async fn create_form(session: Session) -> actix_web::Result<impl Resp
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ObjectModelForm {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
// #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected
|
||||
#[serde(with="my_string_empty_as_none")]
|
||||
pub name_property: Option<ID>,
|
||||
}
|
||||
|
||||
#[post("/model/object/create")]
|
||||
@@ -51,6 +56,7 @@ pub(crate) async fn create(
|
||||
match wg.define_object(ObjectModel {
|
||||
id: Default::default(),
|
||||
name: form.name.clone(),
|
||||
name_property: form.name_property
|
||||
}) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
@@ -84,12 +90,22 @@ pub(crate) async fn update_form(
|
||||
// Re-fill old values
|
||||
if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") {
|
||||
let mut model = model.clone();
|
||||
context.insert("old", &form);
|
||||
model.name = form.name;
|
||||
model.name_property = form.name_property;
|
||||
context.insert("model", &model);
|
||||
} else {
|
||||
context.insert("model", model);
|
||||
context.insert("old", &ObjectModelForm {
|
||||
name: model.name.to_string(),
|
||||
name_property: model.name_property
|
||||
});
|
||||
}
|
||||
|
||||
let properties = rg.get_property_models_for_parent(*model_id).collect_vec();
|
||||
|
||||
context.insert("properties", &properties);
|
||||
|
||||
TERA.build_response("models/model_update", &context)
|
||||
}
|
||||
|
||||
@@ -107,6 +123,7 @@ pub(crate) async fn update(
|
||||
match wg.update_object_model(ObjectModel {
|
||||
id,
|
||||
name: form.name.clone(),
|
||||
name_property: form.name_property,
|
||||
}) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
@@ -144,3 +161,89 @@ pub(crate) async fn delete(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub mod my_string_empty_as_none {
|
||||
use serde::{Deserializer, Serializer, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::marker::PhantomData;
|
||||
use serde::de::{Visitor, Error};
|
||||
use std::fmt;
|
||||
use yopa::ID;
|
||||
|
||||
// FIXME largely copied from serde_with
|
||||
|
||||
/// Deserialize an `Option<T>` from a string using `FromStr`
|
||||
pub fn deserialize<'de, D, S>(deserializer: D) -> Result<Option<S>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
S: FromStr,
|
||||
S::Err: Display,
|
||||
{
|
||||
struct OptionStringEmptyNone<S>(PhantomData<S>);
|
||||
impl<'de, S> Visitor<'de> for OptionStringEmptyNone<S>
|
||||
where
|
||||
S: FromStr,
|
||||
S::Err: Display,
|
||||
{
|
||||
type Value = Option<S>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("any string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
match value {
|
||||
"" => Ok(None),
|
||||
v => S::from_str(v).map(Some).map_err(Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
match &*value {
|
||||
"" => Ok(None),
|
||||
v => S::from_str(v).map(Some).map_err(Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove?
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
self.visit_str(&v.to_string())
|
||||
}
|
||||
|
||||
// handles the `null` case
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(OptionStringEmptyNone(PhantomData))
|
||||
}
|
||||
|
||||
/// Serialize a string from `Option<T>` using `AsRef<str>` or using the empty string if `None`.
|
||||
pub fn serialize<S>(option: &Option<ID>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
option.serialize(serializer)
|
||||
|
||||
// if let Some(value) = option {
|
||||
// value.serialize(serializer)
|
||||
// } else {
|
||||
// "".serialize(serializer)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,13 @@ pub(crate) async fn create_form(
|
||||
} else {
|
||||
context.insert(
|
||||
"old",
|
||||
// This is the defaults for the form
|
||||
&PropertyModelCreateForm {
|
||||
object: Default::default(),
|
||||
name: "".to_string(),
|
||||
optional: true,
|
||||
multiple: false,
|
||||
unique: false,
|
||||
data_type: DataType::String,
|
||||
default: "".to_string(),
|
||||
},
|
||||
@@ -70,6 +72,8 @@ pub(crate) struct PropertyModelCreateForm {
|
||||
pub optional: bool,
|
||||
#[serde(default)]
|
||||
pub multiple: bool,
|
||||
#[serde(default)]
|
||||
pub unique: bool,
|
||||
pub data_type: DataType,
|
||||
/// Default value to be parsed to the data type
|
||||
/// May be unused if empty and optional
|
||||
@@ -126,6 +130,7 @@ pub(crate) async fn create(
|
||||
|
||||
let optional = form.optional;
|
||||
let multiple = form.multiple;
|
||||
let unique = form.unique;
|
||||
|
||||
let default = match parse_default(form.data_type, form.default.clone()) {
|
||||
Ok(def) => def,
|
||||
@@ -143,6 +148,7 @@ pub(crate) async fn create(
|
||||
name: form.name.clone(),
|
||||
optional,
|
||||
multiple,
|
||||
unique,
|
||||
data_type: form.data_type,
|
||||
default,
|
||||
}) {
|
||||
@@ -190,6 +196,8 @@ pub(crate) struct PropertyModelEditForm {
|
||||
pub optional: bool,
|
||||
#[serde(default)]
|
||||
pub multiple: bool,
|
||||
#[serde(default)]
|
||||
pub unique: bool,
|
||||
pub data_type: DataType,
|
||||
/// Default value to be parsed to the data type
|
||||
/// May be unused if empty and optional
|
||||
@@ -253,6 +261,7 @@ pub(crate) async fn update(
|
||||
name: form.name.clone(),
|
||||
optional: form.optional,
|
||||
multiple: form.multiple,
|
||||
unique: form.unique,
|
||||
data_type: form.data_type,
|
||||
default,
|
||||
}) {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use crate::session_ext::SessionExt;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
use crate::TERA;
|
||||
use serde::Serialize;
|
||||
use yopa::data::Object;
|
||||
use yopa::insert::InsertObj;
|
||||
use yopa::{data, model, Storage, ID};
|
||||
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use heck::TitleCase;
|
||||
use itertools::Itertools;
|
||||
use json_dotpath::DotPaths;
|
||||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use yopa::{data, ID, model, Storage};
|
||||
use yopa::data::Object;
|
||||
use yopa::insert::InsertObj;
|
||||
use yopa::model::{ObjectModel, PropertyModel, RelationModel};
|
||||
use yopa::update::UpdateObj;
|
||||
|
||||
use crate::session_ext::SessionExt;
|
||||
use crate::TERA;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
|
||||
// we only need references here, Context serializes everything to Value.
|
||||
// cloning would be a waste of cycles
|
||||
|
||||
@@ -27,11 +29,18 @@ pub struct Schema<'a> {
|
||||
pub prop_models: Vec<&'a model::PropertyModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ObjectDisplay<'a> {
|
||||
pub id: ID,
|
||||
pub model: ID,
|
||||
pub name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ObjectCreateData<'a> {
|
||||
pub model_id: ID,
|
||||
pub schema: Schema<'a>,
|
||||
pub objects: Vec<&'a Object>,
|
||||
pub objects: Vec<ObjectDisplay<'a>>,
|
||||
}
|
||||
|
||||
#[get("/object/create/{model_id}")]
|
||||
@@ -76,16 +85,25 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result<O
|
||||
related_ids.sort();
|
||||
related_ids.dedup();
|
||||
|
||||
let mut models_to_fetch = vec![model_id];
|
||||
models_to_fetch.extend(&related_ids);
|
||||
|
||||
Ok(ObjectCreateData {
|
||||
model_id: model.id,
|
||||
schema: Schema {
|
||||
obj_models: rg.get_object_models().collect(), // TODO get only the ones that matter here
|
||||
obj_models: rg.get_object_models_by_ids(models_to_fetch).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_types(related_ids).collect(),
|
||||
objects: rg.get_objects_of_types(related_ids).map(|o| {
|
||||
ObjectDisplay {
|
||||
id: o.id,
|
||||
model: o.model,
|
||||
name: rg.get_object_name(o),
|
||||
}
|
||||
}).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,22 +115,17 @@ pub(crate) async fn create(
|
||||
) -> 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();
|
||||
|
||||
let model_name = wg.get_model_name(form.model).to_owned().to_title_case();
|
||||
|
||||
match wg.insert_object(form) {
|
||||
Ok(_id) => {
|
||||
Ok(id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
let obj_name = wg.get_object_name_by_id(id);
|
||||
debug!("Object created, redirecting to root");
|
||||
session.flash_success(format!("{} \"{}\" created.", model_name, name));
|
||||
session.flash_success(format!("{} \"{}\" created.", model_name, obj_name));
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -125,7 +138,7 @@ pub(crate) async fn create(
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct ModelWithObjects<'a> {
|
||||
model: &'a ObjectModel,
|
||||
objects: Vec<&'a Object>,
|
||||
objects: Vec<ObjectDisplay<'a>>,
|
||||
}
|
||||
|
||||
#[get("/objects")]
|
||||
@@ -148,8 +161,16 @@ pub(crate) async fn list_inner(
|
||||
.get_object_models()
|
||||
.sorted_by_key(|m| &m.name)
|
||||
.map(|model| {
|
||||
let mut objects = objects_by_model.remove(&model.id).unwrap_or_default();
|
||||
objects.sort_by_key(|o| &o.name);
|
||||
let objects = objects_by_model.remove(&model.id).unwrap_or_default();
|
||||
let mut objects = objects.into_iter().map(|o| {
|
||||
ObjectDisplay {
|
||||
id: o.id,
|
||||
model: o.model,
|
||||
name: rg.get_object_name(o), // TODO optimize
|
||||
}
|
||||
}).collect_vec();
|
||||
|
||||
objects.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
ModelWithObjects { model, objects }
|
||||
})
|
||||
.collect();
|
||||
@@ -176,7 +197,8 @@ struct RelationView<'a> {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RelationInstanceView<'a> {
|
||||
related: &'a Object,
|
||||
related: ObjectDisplay<'a>,
|
||||
related_name: Cow<'a, str>,
|
||||
properties: Vec<PropertyView<'a>>,
|
||||
}
|
||||
|
||||
@@ -201,7 +223,11 @@ pub(crate) async fn detail(
|
||||
.get_object_model(object.model)
|
||||
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
|
||||
|
||||
context.insert("object", object);
|
||||
context.insert("object", &ObjectDisplay {
|
||||
id: object_id,
|
||||
model: object.model,
|
||||
name: rg.get_object_name(object),
|
||||
});
|
||||
context.insert("model", model);
|
||||
context.insert("kind", &rg.get_model_name(object.model));
|
||||
|
||||
@@ -270,13 +296,20 @@ pub(crate) async fn detail(
|
||||
|
||||
view_rel_properties.sort_by_key(|p| &p.model.name);
|
||||
|
||||
let related_name = rg.get_object_name(related_obj);
|
||||
|
||||
instances.push(RelationInstanceView {
|
||||
related: related_obj,
|
||||
related: ObjectDisplay {
|
||||
id: related_obj.id,
|
||||
model: related_obj.model,
|
||||
name: rg.get_object_name(related_obj),
|
||||
},
|
||||
related_name,
|
||||
properties: view_rel_properties,
|
||||
})
|
||||
}
|
||||
|
||||
instances.sort_by_key(|r| &r.related.name);
|
||||
instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
|
||||
|
||||
relation_views.push(RelationView {
|
||||
model: rg.get_relation_model(model_id).unwrap(),
|
||||
@@ -322,13 +355,21 @@ pub(crate) async fn detail(
|
||||
|
||||
view_rel_properties.sort_by_key(|p| &p.model.name);
|
||||
|
||||
let related_name = rg.get_object_name(related_obj);
|
||||
|
||||
instances.push(RelationInstanceView {
|
||||
related: related_obj,
|
||||
related: ObjectDisplay {
|
||||
id: related_obj.id,
|
||||
model: related_obj.model,
|
||||
name: rg.get_object_name(related_obj),
|
||||
},
|
||||
related_name,
|
||||
properties: view_rel_properties,
|
||||
})
|
||||
}
|
||||
|
||||
instances.sort_by_key(|r| &r.related.name);
|
||||
// sort_by_key is not possible because rust is still stupid about lifetimes
|
||||
instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
|
||||
|
||||
relation_views.push(RelationView {
|
||||
model: rg.get_relation_model(model_id).unwrap(),
|
||||
@@ -349,7 +390,7 @@ pub(crate) async fn detail(
|
||||
struct EnrichedObject<'a> {
|
||||
id: ID,
|
||||
model: ID,
|
||||
name: String,
|
||||
name: Cow<'a, str>,
|
||||
values: HashMap<
|
||||
String, /* ID but as string so serde will stop exploding */
|
||||
Vec<&'a data::Value>,
|
||||
@@ -387,7 +428,11 @@ pub(crate) async fn update_form(
|
||||
|
||||
// maybe its useful,idk
|
||||
context.insert("model", &model);
|
||||
context.insert("object", &object);
|
||||
context.insert("object", &ObjectDisplay {
|
||||
id: object.id,
|
||||
model: object.model,
|
||||
name: rg.get_object_name(object),
|
||||
});
|
||||
|
||||
let create_data = prepare_object_create_data(&rg, model.id)?;
|
||||
|
||||
@@ -480,7 +525,7 @@ pub(crate) async fn update_form(
|
||||
let object = EnrichedObject {
|
||||
id: object.id,
|
||||
model: object.model,
|
||||
name: object.name.clone(),
|
||||
name: rg.get_object_name_by_id(object.id),
|
||||
values: value_map,
|
||||
relations: relation_map,
|
||||
};
|
||||
@@ -494,26 +539,26 @@ pub(crate) async fn update_form(
|
||||
|
||||
#[post("/object/update/{id}")]
|
||||
pub(crate) async fn update(
|
||||
_id: web::Path<ID>,
|
||||
id: web::Path<ID>,
|
||||
form: web::Json<UpdateObj>,
|
||||
store: crate::YopaStoreWrapper,
|
||||
session: Session,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
let mut wg = store.write().await;
|
||||
let form = form.into_inner();
|
||||
let name = form.name.clone();
|
||||
|
||||
let object = wg
|
||||
.get_object(form.id)
|
||||
.get_object(*id)
|
||||
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
|
||||
|
||||
let model_name = wg.get_model_name(object.model).to_owned().to_title_case();
|
||||
let id = object.id;
|
||||
|
||||
match wg.update_object(form) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
debug!("Object created, redirecting to root");
|
||||
session.flash_success(format!("{} \"{}\" updated.", model_name, name));
|
||||
session.flash_success(format!("{} \"{}\" updated.", model_name, wg.get_object_name_by_id(id)));
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -530,11 +575,14 @@ pub(crate) async fn delete(
|
||||
session: Session,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let mut wg = store.write().await;
|
||||
|
||||
let name = wg.get_object_name_by_id(*id).to_string();
|
||||
|
||||
match wg.delete_object(*id) {
|
||||
Ok(obj) => {
|
||||
Ok(_obj) => {
|
||||
wg.persist().err_to_500()?;
|
||||
debug!("Object deleted, redirecting to root");
|
||||
session.flash_success(format!("Object \"{}\" deleted.", obj.name));
|
||||
session.flash_success(format!("Object \"{}\" deleted.", name));
|
||||
redirect("/")
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user