Refactor + review feedback

This commit is contained in:
Austin Alvarado
2024-01-19 20:58:59 +00:00
parent ee72b571d0
commit e947b8eef0
12 changed files with 345 additions and 170 deletions

View File

@@ -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>
</>

View File

@@ -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! {

View 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>
}
}

View 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>
}
}

View File

@@ -0,0 +1,4 @@
pub mod checkbox;
pub mod field;
pub mod select;
pub mod submit;

View 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>
}
}

View 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>
}
}

View File

@@ -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;

View File

@@ -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>;

View File

@@ -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>
}
}

View File

@@ -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
View 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,
}
}
}
};
}