diff --git a/app/queries/create_group_attribute.graphql b/app/queries/create_group_attribute.graphql new file mode 100644 index 0000000..10615ac --- /dev/null +++ b/app/queries/create_group_attribute.graphql @@ -0,0 +1,5 @@ +mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!) { + addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: false) { + ok + } +} \ No newline at end of file diff --git a/app/queries/delete_group_attribute.graphql b/app/queries/delete_group_attribute.graphql new file mode 100644 index 0000000..7a52b32 --- /dev/null +++ b/app/queries/delete_group_attribute.graphql @@ -0,0 +1,5 @@ +mutation DeleteGroupAttributeQuery($name: String!) { + deleteGroupAttribute(name: $name) { + ok + } +} \ No newline at end of file diff --git a/app/queries/get_group_attributes_schema.graphql b/app/queries/get_group_attributes_schema.graphql new file mode 100644 index 0000000..979591d --- /dev/null +++ b/app/queries/get_group_attributes_schema.graphql @@ -0,0 +1,13 @@ +query GetGroupAttributesSchema { + schema { + groupSchema { + attributes { + name + attributeType + isList + isVisible + isHardcoded + } + } + } +} diff --git a/app/src/components/app.rs b/app/src/components/app.rs index fbb48c1..b9856c0 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -2,9 +2,11 @@ use crate::{ components::{ change_password::ChangePasswordForm, create_group::CreateGroupForm, + create_group_attribute::CreateGroupAttributeForm, create_user::CreateUserForm, create_user_attribute::CreateUserAttributeForm, group_details::GroupDetails, + group_schema_table::ListGroupSchema, group_table::GroupTable, login::LoginForm, logout::LogoutButton, @@ -232,6 +234,9 @@ impl App { AppRoute::CreateUserAttribute => html! { }, + AppRoute::CreateGroupAttribute => html! { + + }, AppRoute::ListGroups => html! {
@@ -244,6 +249,9 @@ impl App { AppRoute::ListUserSchema => html! { }, + AppRoute::ListGroupSchema => html! { + + }, AppRoute::GroupDetails { group_id } => html! { }, @@ -307,6 +315,14 @@ impl App { {"User schema"} +
  • + + + {"Group schema"} + +
  • } } else { html!{} } } diff --git a/app/src/components/create_group_attribute.rs b/app/src/components/create_group_attribute.rs new file mode 100644 index 0000000..1ae1562 --- /dev/null +++ b/app/src/components/create_group_attribute.rs @@ -0,0 +1,168 @@ +use crate::{ + components::{ + form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit}, + router::AppRoute, + }, + convert_attribute_type, + infra::{ + common_component::{CommonComponent, CommonComponentParts}, + schema::{validate_attribute_type, AttributeType}, + }, +}; +use anyhow::{bail, Result}; +use gloo_console::log; +use graphql_client::GraphQLQuery; +use validator_derive::Validate; +use yew::prelude::*; +use yew_form_derive::Model; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/create_group_attribute.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct CreateGroupAttribute; + +convert_attribute_type!(create_group_attribute::AttributeType); + +pub struct CreateGroupAttributeForm { + common: CommonComponentParts, + form: yew_form::Form, +} + +#[derive(Model, Validate, PartialEq, Eq, Clone, Default, Debug)] +pub struct CreateGroupAttributeModel { + #[validate(length(min = 1, message = "attribute_name is required"))] + attribute_name: String, + #[validate(custom = "validate_attribute_type")] + attribute_type: String, + is_list: bool, + is_visible: bool, // remove when backend doesn't return group attributes for normal users +} + +pub enum Msg { + Update, + SubmitForm, + CreateGroupAttributeResponse(Result), +} + +impl CommonComponent for CreateGroupAttributeForm { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + match msg { + Msg::Update => Ok(true), + Msg::SubmitForm => { + if !self.form.validate() { + bail!("Check the form for errors"); + } + let model = self.form.model(); + let attribute_type = model.attribute_type.parse::().unwrap(); + let req = create_group_attribute::Variables { + name: model.attribute_name, + attribute_type: create_group_attribute::AttributeType::from(attribute_type), + is_list: model.is_list, + is_visible: model.is_visible, + }; + self.common.call_graphql::( + ctx, + req, + Msg::CreateGroupAttributeResponse, + "Error trying to create group attribute", + ); + Ok(true) + } + Msg::CreateGroupAttributeResponse(response) => { + response?; + let model = self.form.model(); + log!(&format!( + "Created group attribute '{}'", + model.attribute_name + )); + ctx.link() + .history() + .unwrap() + .push(AppRoute::ListGroupSchema); + Ok(true) + } + } + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for CreateGroupAttributeForm { + type Message = Msg; + type Properties = (); + + fn create(_: &Context) -> Self { + let model = CreateGroupAttributeModel { + attribute_type: AttributeType::String.to_string(), + ..Default::default() + }; + Self { + common: CommonComponentParts::::create(), + form: yew_form::Form::::new(model), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) + } + + fn view(&self, ctx: &Context) -> Html { + let link = ctx.link(); + html! { +
    +
    +
    {"Create a group attribute"}
    + + label="Name" + required={true} + form={&self.form} + field_name="attribute_name" + oninput={link.callback(|_| Msg::Update)} /> + + label="Type" + required={true} + form={&self.form} + field_name="attribute_type" + oninput={link.callback(|_| Msg::Update)}> + + + + + > + + label="Multiple values" + form={&self.form} + field_name="is_list" + ontoggle={link.callback(|_| Msg::Update)} /> + + label="Visible to users" + form={&self.form} + field_name="is_visible" + ontoggle={link.callback(|_| Msg::Update)} /> + + + { if let Some(e) = &self.common.error { + html! { +
    + {e.to_string() } +
    + } + } else { html! {} } + } +
    + } + } +} diff --git a/app/src/components/create_user_attribute.rs b/app/src/components/create_user_attribute.rs index 569c457..88c78fe 100644 --- a/app/src/components/create_user_attribute.rs +++ b/app/src/components/create_user_attribute.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::{ components::{ form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit}, @@ -8,13 +6,12 @@ use crate::{ convert_attribute_type, infra::{ common_component::{CommonComponent, CommonComponentParts}, - schema::AttributeType, + schema::{validate_attribute_type, AttributeType}, }, }; use anyhow::{bail, Result}; use gloo_console::log; use graphql_client::GraphQLQuery; -use validator::ValidationError; use validator_derive::Validate; use yew::prelude::*; use yew_form_derive::Model; @@ -47,14 +44,6 @@ pub struct CreateUserAttributeModel { is_visible: bool, } -fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> { - let result = AttributeType::from_str(attribute_type); - match result { - Ok(_) => Ok(()), - _ => Err(ValidationError::new("Invalid attribute type")), - } -} - pub enum Msg { Update, SubmitForm, @@ -146,7 +135,7 @@ impl Component for CreateUserAttributeForm { label="Type" required={true} form={&self.form} - field_name="attribute_name" + field_name="attribute_type" oninput={link.callback(|_| Msg::Update)}> diff --git a/app/src/components/delete_group_attribute.rs b/app/src/components/delete_group_attribute.rs new file mode 100644 index 0000000..435ee58 --- /dev/null +++ b/app/src/components/delete_group_attribute.rs @@ -0,0 +1,172 @@ +use crate::infra::{ + common_component::{CommonComponent, CommonComponentParts}, + modal::Modal, +}; +use anyhow::{Error, Result}; +use graphql_client::GraphQLQuery; +use yew::prelude::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/delete_group_attribute.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct DeleteGroupAttributeQuery; + +pub struct DeleteGroupAttribute { + common: CommonComponentParts, + node_ref: NodeRef, + modal: Option, +} + +#[derive(yew::Properties, Clone, PartialEq, Debug)] +pub struct DeleteGroupAttributeProps { + pub attribute_name: String, + pub on_attribute_deleted: Callback, + pub on_error: Callback, +} + +pub enum Msg { + ClickedDeleteGroupAttribute, + ConfirmDeleteGroupAttribute, + DismissModal, + DeleteGroupAttributeResponse(Result), +} + +impl CommonComponent for DeleteGroupAttribute { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + match msg { + Msg::ClickedDeleteGroupAttribute => { + self.modal.as_ref().expect("modal not initialized").show(); + } + Msg::ConfirmDeleteGroupAttribute => { + self.update(ctx, Msg::DismissModal); + self.common.call_graphql::( + ctx, + delete_group_attribute_query::Variables { + name: ctx.props().attribute_name.clone(), + }, + Msg::DeleteGroupAttributeResponse, + "Error trying to delete group attribute", + ); + } + Msg::DismissModal => { + self.modal.as_ref().expect("modal not initialized").hide(); + } + Msg::DeleteGroupAttributeResponse(response) => { + response?; + ctx.props() + .on_attribute_deleted + .emit(ctx.props().attribute_name.clone()); + } + } + Ok(true) + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for DeleteGroupAttribute { + type Message = Msg; + type Properties = DeleteGroupAttributeProps; + + fn create(_: &Context) -> Self { + Self { + common: CommonComponentParts::::create(), + node_ref: NodeRef::default(), + modal: None, + } + } + + fn rendered(&mut self, _: &Context, first_render: bool) { + if first_render { + self.modal = Some(Modal::new( + self.node_ref + .cast::() + .expect("Modal node is not an element"), + )); + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update_and_report_error( + self, + ctx, + msg, + ctx.props().on_error.clone(), + ) + } + + fn view(&self, ctx: &Context) -> Html { + let link = &ctx.link(); + html! { + <> + + {self.show_modal(ctx)} + + } + } +} + +impl DeleteGroupAttribute { + fn show_modal(&self, ctx: &Context) -> Html { + let link = &ctx.link(); + html! { + + } + } +} diff --git a/app/src/components/group_schema_table.rs b/app/src/components/group_schema_table.rs new file mode 100644 index 0000000..0a6a4f7 --- /dev/null +++ b/app/src/components/group_schema_table.rs @@ -0,0 +1,198 @@ +use crate::{ + components::{ + delete_group_attribute::DeleteGroupAttribute, + router::{AppRoute, Link}, + }, + convert_attribute_type, + infra::{ + common_component::{CommonComponent, CommonComponentParts}, + schema::AttributeType, + }, +}; +use anyhow::{anyhow, Error, Result}; +use gloo_console::log; +use graphql_client::GraphQLQuery; +use yew::prelude::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/get_group_attributes_schema.graphql", + response_derives = "Debug,Clone,PartialEq,Eq", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct GetGroupAttributesSchema; + +use get_group_attributes_schema::ResponseData; + +pub type Attribute = + get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes; + +convert_attribute_type!(get_group_attributes_schema::AttributeType); + +#[derive(yew::Properties, Clone, PartialEq, Eq)] +pub struct Props { + pub hardcoded: bool, +} + +pub struct GroupSchemaTable { + common: CommonComponentParts, + attributes: Option>, +} + +pub enum Msg { + ListAttributesResponse(Result), + OnAttributeDeleted(String), + OnError(Error), +} + +impl CommonComponent for GroupSchemaTable { + fn handle_msg(&mut self, _: &Context, msg: ::Message) -> Result { + match msg { + Msg::ListAttributesResponse(schema) => { + self.attributes = + Some(schema?.schema.group_schema.attributes.into_iter().collect()); + Ok(true) + } + Msg::OnError(e) => Err(e), + Msg::OnAttributeDeleted(attribute_name) => { + match self.attributes { + None => { + log!(format!("Attribute {attribute_name} was deleted but component has no attributes")); + Err(anyhow!("invalid state")) + } + Some(_) => { + self.attributes + .as_mut() + .unwrap() + .retain(|a| a.name != attribute_name); + Ok(true) + } + } + } + } + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for GroupSchemaTable { + type Message = Msg; + type Properties = Props; + + fn create(ctx: &Context) -> Self { + let mut table = GroupSchemaTable { + common: CommonComponentParts::::create(), + attributes: None, + }; + table.common.call_graphql::( + ctx, + get_group_attributes_schema::Variables {}, + Msg::ListAttributesResponse, + "Error trying to fetch group schema", + ); + table + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
    + {self.view_attributes(ctx)} + {self.view_errors()} +
    + } + } +} + +impl GroupSchemaTable { + fn view_attributes(&self, ctx: &Context) -> Html { + let hardcoded = ctx.props().hardcoded; + let make_table = |attributes: &Vec| { + html! { +
    +

    {if hardcoded {"Hardcoded"} else {"User-defined"}}{" attributes"}

    + + + + + + + {if hardcoded {html!{}} else {html!{}}} + + + + {attributes.iter().map(|u| self.view_attribute(ctx, u)).collect::>()} + +
    {"Attribute name"}{"Type"}{"Visible"}{"Delete"}
    +
    + } + }; + match &self.attributes { + None => html! {{"Loading..."}}, + Some(attributes) => { + let mut attributes = attributes.clone(); + attributes.retain(|attribute| attribute.is_hardcoded == ctx.props().hardcoded); + make_table(&attributes) + } + } + } + + fn view_attribute(&self, ctx: &Context, attribute: &Attribute) -> Html { + let link = ctx.link(); + let attribute_type = AttributeType::from(attribute.attribute_type.clone()); + let checkmark = html! { + + + + }; + let hardcoded = ctx.props().hardcoded; + html! { + + {&attribute.name} + {if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}} + {if attribute.is_visible {checkmark.clone()} else {html!{}}} + { + if hardcoded { + html!{} + } else { + html!{ + + + + } + } + } + + } + } + + fn view_errors(&self) -> Html { + match &self.common.error { + None => html! {}, + Some(e) => html! {
    {"Error: "}{e.to_string()}
    }, + } + } +} + +#[function_component(ListGroupSchema)] +pub fn list_group_schema() -> Html { + html! { +
    + + + + + {"Create an attribute"} + +
    + } +} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs index 0fe1c13..5229f44 100644 --- a/app/src/components/mod.rs +++ b/app/src/components/mod.rs @@ -3,13 +3,16 @@ pub mod add_user_to_group; pub mod app; pub mod change_password; pub mod create_group; +pub mod create_group_attribute; pub mod create_user; pub mod create_user_attribute; pub mod delete_group; +pub mod delete_group_attribute; pub mod delete_user; pub mod delete_user_attribute; pub mod form; pub mod group_details; +pub mod group_schema_table; pub mod group_table; pub mod login; pub mod logout; diff --git a/app/src/components/router.rs b/app/src/components/router.rs index 09e7782..172ded0 100644 --- a/app/src/components/router.rs +++ b/app/src/components/router.rs @@ -26,6 +26,10 @@ pub enum AppRoute { ListUserSchema, #[at("/user-attributes/create")] CreateUserAttribute, + #[at("/group-attributes")] + ListGroupSchema, + #[at("/group-attributes/create")] + CreateGroupAttribute, #[at("/")] Index, } diff --git a/app/src/infra/schema.rs b/app/src/infra/schema.rs index 7aecc9e..f59bc76 100644 --- a/app/src/infra/schema.rs +++ b/app/src/infra/schema.rs @@ -1,5 +1,6 @@ use anyhow::Result; use std::{fmt::Display, str::FromStr}; +use validator::ValidationError; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AttributeType { @@ -58,6 +59,7 @@ macro_rules! convert_attribute_type { }; } +<<<<<<< HEAD #[derive(Clone, PartialEq, Eq)] pub struct Attribute { pub name: String, @@ -85,4 +87,10 @@ macro_rules! combine_schema_and_values { } }).collect(); }; +======= +pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> { + AttributeType::from_str(attribute_type) + .map_err(|_| ValidationError::new("Invalid attribute type"))?; + Ok(()) +>>>>>>> 8f2391a (app: create group attribute schema page (#825)) }