Refactor + review feedback
This commit is contained in:
@@ -11,8 +11,8 @@ use crate::{
|
||||
reset_password_step1::ResetPasswordStep1Form,
|
||||
reset_password_step2::ResetPasswordStep2Form,
|
||||
router::{AppRoute, Link, Redirect},
|
||||
user_attributes_table::UserAttributesTable,
|
||||
user_details::UserDetails,
|
||||
user_schema_table::ListUserSchema,
|
||||
user_table::UserTable,
|
||||
},
|
||||
infra::{api::HostService, cookies::get_cookie},
|
||||
@@ -241,14 +241,8 @@ impl App {
|
||||
</Link>
|
||||
</div>
|
||||
},
|
||||
AppRoute::ListUserAttributes => html! {
|
||||
<div>
|
||||
<UserAttributesTable />
|
||||
<Link classes="btn btn-primary" to={AppRoute::CreateUserAttribute}>
|
||||
<i class="bi-plus-circle me-2"></i>
|
||||
{"Create an attribute"}
|
||||
</Link>
|
||||
</div>
|
||||
AppRoute::ListUserSchema => html! {
|
||||
<ListUserSchema />
|
||||
},
|
||||
AppRoute::GroupDetails { group_id } => html! {
|
||||
<GroupDetails group_id={*group_id} />
|
||||
@@ -308,9 +302,9 @@ impl App {
|
||||
<li>
|
||||
<Link
|
||||
classes="nav-link px-2 h6"
|
||||
to={AppRoute::ListUserAttributes}>
|
||||
to={AppRoute::ListUserSchema}>
|
||||
<i class="bi-list-ul me-2"></i>
|
||||
{"User attributes"}
|
||||
{"User schema"}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
use crate::{
|
||||
components::router::AppRoute,
|
||||
infra::common_component::{CommonComponent, CommonComponentParts},
|
||||
components::{
|
||||
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
|
||||
router::AppRoute,
|
||||
},
|
||||
convert_attribute_type,
|
||||
infra::{
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
schema::AttributeType,
|
||||
},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use gloo_console::log;
|
||||
@@ -19,7 +26,7 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
|
||||
)]
|
||||
pub struct CreateUserAttribute;
|
||||
|
||||
type AttributeType = create_user_attribute::AttributeType;
|
||||
convert_attribute_type!(create_user_attribute::AttributeType);
|
||||
|
||||
pub struct CreateUserAttributeForm {
|
||||
common: CommonComponentParts<Self>,
|
||||
@@ -59,16 +66,10 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
|
||||
if model.is_editable && !model.is_visible {
|
||||
bail!("Editable attributes must also be visible");
|
||||
}
|
||||
let attribute_type = match model.attribute_type.as_str() {
|
||||
"Jpeg" => AttributeType::JPEG_PHOTO,
|
||||
"DateTime" => AttributeType::DATE_TIME,
|
||||
"Integer" => AttributeType::INTEGER,
|
||||
"String" => AttributeType::STRING,
|
||||
_ => bail!("Check the form for errors"),
|
||||
};
|
||||
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
|
||||
let req = create_user_attribute::Variables {
|
||||
name: model.attribute_name,
|
||||
attribute_type,
|
||||
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
|
||||
is_editable: model.is_editable,
|
||||
is_list: model.is_list,
|
||||
is_visible: model.is_visible,
|
||||
@@ -88,10 +89,7 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
|
||||
"Created user attribute '{}'",
|
||||
model.attribute_name
|
||||
));
|
||||
ctx.link()
|
||||
.history()
|
||||
.unwrap()
|
||||
.push(AppRoute::ListUserAttributes);
|
||||
ctx.link().history().unwrap().push(AppRoute::ListUserSchema);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +106,7 @@ impl Component for CreateUserAttributeForm {
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
let model = CreateUserAttributeModel {
|
||||
attribute_type: "String".to_string(),
|
||||
attribute_type: AttributeType::String.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
Self {
|
||||
@@ -123,105 +121,45 @@ impl Component for CreateUserAttributeForm {
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
type Field = yew_form::Field<CreateUserAttributeModel>;
|
||||
type Select = yew_form::Select<CreateUserAttributeModel>;
|
||||
type Checkbox = yew_form::CheckBox<CreateUserAttributeModel>;
|
||||
html! {
|
||||
<div class="row justify-content-center">
|
||||
<form class="form py-3" style="max-width: 636px">
|
||||
<div class="row mb-3">
|
||||
<h5 class="fw-bold">{"Create a user attribute"}</h5>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="attribute_name"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Attribute name"}
|
||||
<span class="text-danger">{"*"}</span>
|
||||
{":"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<Field
|
||||
form={&self.form}
|
||||
field_name="attribute_name"
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
autocomplete="attribute_name"
|
||||
oninput={link.callback(|_| Msg::Update)} />
|
||||
<div class="invalid-feedback">
|
||||
{&self.form.field_message("attribute_name")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="attribute_type"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Type:"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<Select
|
||||
form={&self.form}
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
field_name="attribute_type"
|
||||
oninput={link.callback(|_| Msg::Update)} >
|
||||
<option selected=true value="String">{"String"}</option>
|
||||
<option value="Integer">{"Integer"}</option>
|
||||
<option value="Jpeg">{"Jpeg"}</option>
|
||||
<option value="DateTime">{"DateTime"}</option>
|
||||
</Select>
|
||||
<div class="invalid-feedback">
|
||||
{&self.form.field_message("attribute_type")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="is_list"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Multiple values:"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<Checkbox
|
||||
form={&self.form}
|
||||
field_name="is_list"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="is_visible"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Visible to users:"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<Checkbox
|
||||
form={&self.form}
|
||||
field_name="is_visible"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="is_editable"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Editable by users:"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<Checkbox
|
||||
form={&self.form}
|
||||
field_name="is_editable"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row justify-content-center">
|
||||
<button
|
||||
class="btn btn-primary col-auto col-form-label"
|
||||
type="submit"
|
||||
disabled={self.common.is_task_running()}
|
||||
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
|
||||
<i class="bi-save me-2"></i>
|
||||
{"Submit"}
|
||||
</button>
|
||||
</div>
|
||||
<h5 class="fw-bold">{"Create a user attribute"}</h5>
|
||||
<Field<CreateUserAttributeModel>
|
||||
label="Name"
|
||||
required={true}
|
||||
form={&self.form}
|
||||
field_name="attribute_name"
|
||||
oninput={link.callback(|_| Msg::Update)} />
|
||||
<Select<CreateUserAttributeModel>
|
||||
label="Type"
|
||||
required={true}
|
||||
form={&self.form}
|
||||
field_name="attribute_name"
|
||||
oninput={link.callback(|_| Msg::Update)}>
|
||||
<option selected=true value="String">{"String"}</option>
|
||||
<option value="Integer">{"Integer"}</option>
|
||||
<option value="Jpeg">{"Jpeg"}</option>
|
||||
<option value="DateTime">{"DateTime"}</option>
|
||||
</Select<CreateUserAttributeModel>>
|
||||
<CheckBox<CreateUserAttributeModel>
|
||||
label="Multiple values"
|
||||
form={&self.form}
|
||||
field_name="is_list"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
<CheckBox<CreateUserAttributeModel>
|
||||
label="Visible to users"
|
||||
form={&self.form}
|
||||
field_name="is_visible"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
<CheckBox<CreateUserAttributeModel>
|
||||
label="Editable by users"
|
||||
form={&self.form}
|
||||
field_name="is_editable"
|
||||
ontoggle={link.callback(|_| Msg::Update)} />
|
||||
<Submit
|
||||
disabled={self.common.is_task_running()}
|
||||
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}/>
|
||||
</form>
|
||||
{ if let Some(e) = &self.common.error {
|
||||
html! {
|
||||
|
||||
35
app/src/components/form/checkbox.rs
Normal file
35
app/src/components/form/checkbox.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Properties};
|
||||
use yew_form::{Form, Model};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props<T: Model> {
|
||||
pub label: AttrValue,
|
||||
pub field_name: String,
|
||||
pub form: Form<T>,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub ontoggle: Callback<bool>,
|
||||
}
|
||||
|
||||
#[function_component(CheckBox)]
|
||||
pub fn checkbox<T: Model>(props: &Props<T>) -> Html {
|
||||
html! {
|
||||
<div class="form-group row mb-3">
|
||||
<label for={props.field_name.clone()}
|
||||
class="form-label col-4 col-form-label">
|
||||
{&props.label}
|
||||
{if props.required {
|
||||
html!{<span class="text-danger">{"*"}</span>}
|
||||
} else {html!{}}}
|
||||
{":"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<yew_form::CheckBox<T>
|
||||
form={&props.form}
|
||||
field_name={props.field_name.clone()}
|
||||
ontoggle={props.ontoggle.clone()} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
42
app/src/components/form/field.rs
Normal file
42
app/src/components/form/field.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use yew::{function_component, html, virtual_dom::AttrValue, Callback, InputEvent, Properties};
|
||||
use yew_form::{Form, Model};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props<T: Model> {
|
||||
pub label: AttrValue,
|
||||
pub field_name: String,
|
||||
pub form: Form<T>,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub oninput: Callback<InputEvent>,
|
||||
}
|
||||
|
||||
#[function_component(Field)]
|
||||
pub fn field<T: Model>(props: &Props<T>) -> Html {
|
||||
html! {
|
||||
<div class="row mb-3">
|
||||
<label for={props.field_name.clone()}
|
||||
class="form-label col-4 col-form-label">
|
||||
{&props.label}
|
||||
{if props.required {
|
||||
html!{<span class="text-danger">{"*"}</span>}
|
||||
} else {html!{}}}
|
||||
{":"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<yew_form::Field<T>
|
||||
form={&props.form}
|
||||
field_name={props.field_name.clone()}
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
autocomplete={props.field_name.clone()}
|
||||
oninput={&props.oninput} />
|
||||
<div class="invalid-feedback">
|
||||
{&props.form.field_message(&props.field_name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
4
app/src/components/form/mod.rs
Normal file
4
app/src/components/form/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod checkbox;
|
||||
pub mod field;
|
||||
pub mod select;
|
||||
pub mod submit;
|
||||
46
app/src/components/form/select.rs
Normal file
46
app/src/components/form/select.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use yew::{
|
||||
function_component, html, virtual_dom::AttrValue, Callback, Children, InputEvent, Properties,
|
||||
};
|
||||
use yew_form::{Form, Model};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props<T: Model> {
|
||||
pub label: AttrValue,
|
||||
pub field_name: String,
|
||||
pub form: Form<T>,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub oninput: Callback<InputEvent>,
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[function_component(Select)]
|
||||
pub fn select<T: Model>(props: &Props<T>) -> Html {
|
||||
html! {
|
||||
<div class="row mb-3">
|
||||
<label for={props.field_name.clone()}
|
||||
class="form-label col-4 col-form-label">
|
||||
{&props.label}
|
||||
{if props.required {
|
||||
html!{<span class="text-danger">{"*"}</span>}
|
||||
} else {html!{}}}
|
||||
{":"}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<yew_form::Select<T>
|
||||
form={&props.form}
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
field_name={props.field_name.clone()}
|
||||
oninput={&props.oninput} >
|
||||
{for props.children.iter()}
|
||||
</yew_form::Select<T>>
|
||||
<div class="invalid-feedback">
|
||||
{&props.form.field_message(&props.field_name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
24
app/src/components/form/submit.rs
Normal file
24
app/src/components/form/submit.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use web_sys::MouseEvent;
|
||||
use yew::{function_component, html, Callback, Properties};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props {
|
||||
pub disabled: bool,
|
||||
pub onclick: Callback<MouseEvent>,
|
||||
}
|
||||
|
||||
#[function_component(Submit)]
|
||||
pub fn submit(props: &Props) -> Html {
|
||||
html! {
|
||||
<div class="form-group row justify-content-center">
|
||||
<button
|
||||
class="btn btn-primary col-auto col-form-label"
|
||||
type="submit"
|
||||
disabled={props.disabled}
|
||||
onclick={&props.onclick}>
|
||||
<i class="bi-save me-2"></i>
|
||||
{"Submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod create_user_attribute;
|
||||
pub mod delete_group;
|
||||
pub mod delete_user;
|
||||
pub mod delete_user_attribute;
|
||||
pub mod form;
|
||||
pub mod group_details;
|
||||
pub mod group_table;
|
||||
pub mod login;
|
||||
@@ -17,7 +18,7 @@ pub mod reset_password_step1;
|
||||
pub mod reset_password_step2;
|
||||
pub mod router;
|
||||
pub mod select;
|
||||
pub mod user_attributes_table;
|
||||
pub mod user_details;
|
||||
pub mod user_details_form;
|
||||
pub mod user_schema_table;
|
||||
pub mod user_table;
|
||||
|
||||
@@ -22,12 +22,12 @@ pub enum AppRoute {
|
||||
ListGroups,
|
||||
#[at("/group/:group_id")]
|
||||
GroupDetails { group_id: i64 },
|
||||
#[at("/")]
|
||||
Index,
|
||||
#[at("/user-attributes")]
|
||||
ListUserAttributes,
|
||||
ListUserSchema,
|
||||
#[at("/user-attributes/create")]
|
||||
CreateUserAttribute,
|
||||
#[at("/")]
|
||||
Index,
|
||||
}
|
||||
|
||||
pub type Link = yew_router::components::Link<AppRoute>;
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
components::delete_user_attribute::DeleteUserAttribute,
|
||||
infra::common_component::{CommonComponent, CommonComponentParts},
|
||||
components::{
|
||||
delete_user_attribute::DeleteUserAttribute,
|
||||
router::{AppRoute, Link},
|
||||
},
|
||||
convert_attribute_type,
|
||||
infra::{
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
schema::AttributeType,
|
||||
},
|
||||
};
|
||||
use anyhow::{Error, Result};
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use gloo_console::log;
|
||||
use graphql_client::GraphQLQuery;
|
||||
use yew::prelude::*;
|
||||
|
||||
@@ -18,9 +28,20 @@ pub struct GetUserAttributesSchema;
|
||||
use get_user_attributes_schema::ResponseData;
|
||||
|
||||
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
|
||||
pub type AttributeType = get_user_attributes_schema::AttributeType;
|
||||
|
||||
pub struct UserAttributesTable {
|
||||
convert_attribute_type!(get_user_attributes_schema::AttributeType);
|
||||
|
||||
fn sort_with_hardcoded_first(a: &Attribute, b: &Attribute) -> Ordering {
|
||||
if a.is_hardcoded && !b.is_hardcoded {
|
||||
Ordering::Less
|
||||
} else if !a.is_hardcoded && b.is_hardcoded {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.name.cmp(&b.name)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserSchemaTable {
|
||||
common: CommonComponentParts<Self>,
|
||||
attributes: Option<Vec<Attribute>>,
|
||||
}
|
||||
@@ -31,7 +52,7 @@ pub enum Msg {
|
||||
OnError(Error),
|
||||
}
|
||||
|
||||
impl CommonComponent<UserAttributesTable> for UserAttributesTable {
|
||||
impl CommonComponent<UserSchemaTable> for UserSchemaTable {
|
||||
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
|
||||
match msg {
|
||||
Msg::ListAttributesResponse(schema) => {
|
||||
@@ -39,14 +60,19 @@ impl CommonComponent<UserAttributesTable> for UserAttributesTable {
|
||||
Ok(true)
|
||||
}
|
||||
Msg::OnError(e) => Err(e),
|
||||
Msg::OnAttributeDeleted(attribute_name) => {
|
||||
debug_assert!(self.attributes.is_some());
|
||||
self.attributes
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.retain(|a| a.name != attribute_name);
|
||||
Ok(true)
|
||||
}
|
||||
Msg::OnAttributeDeleted(attribute_name) => match self.attributes {
|
||||
None => {
|
||||
log!("Attribute deleted but component has no attributes");
|
||||
Err(anyhow!("invalid state"))
|
||||
}
|
||||
Some(_) => {
|
||||
self.attributes
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.retain(|a| a.name != attribute_name);
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +81,12 @@ impl CommonComponent<UserAttributesTable> for UserAttributesTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UserAttributesTable {
|
||||
impl Component for UserSchemaTable {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let mut table = UserAttributesTable {
|
||||
let mut table = UserSchemaTable {
|
||||
common: CommonComponentParts::<Self>::create(),
|
||||
attributes: None,
|
||||
};
|
||||
@@ -87,7 +113,7 @@ impl Component for UserAttributesTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttributesTable {
|
||||
impl UserSchemaTable {
|
||||
fn view_attributes(&self, ctx: &Context<Self>) -> Html {
|
||||
let make_table = |attributes: &Vec<Attribute>| {
|
||||
html! {
|
||||
@@ -111,43 +137,35 @@ impl UserAttributesTable {
|
||||
};
|
||||
match &self.attributes {
|
||||
None => html! {{"Loading..."}},
|
||||
Some(attributes) => make_table(attributes),
|
||||
Some(attributes) => {
|
||||
let mut attributes = attributes.clone();
|
||||
attributes.sort_by(sort_with_hardcoded_first);
|
||||
make_table(&attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
|
||||
let link = ctx.link();
|
||||
let attribute_type = match attribute.attribute_type {
|
||||
AttributeType::STRING => "String",
|
||||
AttributeType::INTEGER => "Integer",
|
||||
AttributeType::JPEG_PHOTO => "Jpeg",
|
||||
AttributeType::DATE_TIME => "DateTime",
|
||||
_ => "Unknown",
|
||||
};
|
||||
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
|
||||
let checkmark = html! {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
|
||||
</svg>
|
||||
};
|
||||
html! {
|
||||
<tr key={attribute.name.clone()}>
|
||||
<td>{&attribute.name}</td>
|
||||
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
||||
<td>{if attribute.is_editable {checkmark.clone()} else {html!{}}}</td>
|
||||
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
||||
<td>{if attribute.is_hardcoded {html!{
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
disabled=true>
|
||||
<i class="bi-x-circle-fill" aria-label="Delete attribute" />
|
||||
</button>
|
||||
}} else {html!{
|
||||
<DeleteUserAttribute
|
||||
attribute_name={attribute.name.clone()}
|
||||
on_attribute_deleted={link.callback(Msg::OnAttributeDeleted)}
|
||||
on_error={link.callback(Msg::OnError)}/>
|
||||
}}}</td>
|
||||
</tr>
|
||||
<tr key={attribute.name.clone()}>
|
||||
<td>{&attribute.name}</td>
|
||||
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
||||
<td>{if attribute.is_editable {checkmark.clone()} else {html!{}}}</td>
|
||||
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
||||
<td>{if attribute.is_hardcoded {html!{}} else { html!{
|
||||
<DeleteUserAttribute
|
||||
attribute_name={attribute.name.clone()}
|
||||
on_attribute_deleted={link.callback(Msg::OnAttributeDeleted)}
|
||||
on_error={link.callback(Msg::OnError)}/>
|
||||
}}}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +176,16 @@ impl UserAttributesTable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(ListUserSchema)]
|
||||
pub fn list_user_schema() -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<UserSchemaTable />
|
||||
<Link classes="btn btn-primary" to={AppRoute::CreateUserAttribute}>
|
||||
<i class="bi-plus-circle me-2"></i>
|
||||
{"Create an attribute"}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@ pub mod common_component;
|
||||
pub mod cookies;
|
||||
pub mod graphql;
|
||||
pub mod modal;
|
||||
pub mod schema;
|
||||
|
||||
59
app/src/infra/schema.rs
Normal file
59
app/src/infra/schema.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AttributeType {
|
||||
String,
|
||||
Integer,
|
||||
DateTime,
|
||||
Jpeg,
|
||||
}
|
||||
|
||||
impl Display for AttributeType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AttributeType {
|
||||
type Err = ();
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"String" => Ok(AttributeType::String),
|
||||
"Integer" => Ok(AttributeType::Integer),
|
||||
"DateTime" => Ok(AttributeType::DateTime),
|
||||
"Jpeg" => Ok(AttributeType::Jpeg),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Macro to generate traits for converting between AttributeType and the
|
||||
// graphql generated equivalents.
|
||||
#[macro_export]
|
||||
macro_rules! convert_attribute_type {
|
||||
($source_type:ty) => {
|
||||
impl From<$source_type> for AttributeType {
|
||||
fn from(value: $source_type) -> Self {
|
||||
match value {
|
||||
<$source_type>::STRING => AttributeType::String,
|
||||
<$source_type>::INTEGER => AttributeType::Integer,
|
||||
<$source_type>::DATE_TIME => AttributeType::DateTime,
|
||||
<$source_type>::JPEG_PHOTO => AttributeType::Jpeg,
|
||||
_ => panic!("Unknown attribute type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttributeType> for $source_type {
|
||||
fn from(value: AttributeType) -> Self {
|
||||
match value {
|
||||
AttributeType::String => <$source_type>::STRING,
|
||||
AttributeType::Integer => <$source_type>::INTEGER,
|
||||
AttributeType::DateTime => <$source_type>::DATE_TIME,
|
||||
AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user