app: simplify attribute_type handling, display creation time in user details

In the user table it's still only the date, but that makes sense for an overview
This commit is contained in:
Valentin Tolmer
2025-07-21 22:56:31 +02:00
committed by nitnelave
parent 78337bce72
commit da525fc99b
12 changed files with 58 additions and 104 deletions

View File

@@ -7,7 +7,6 @@ use crate::{
},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{
@@ -30,7 +29,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
@@ -39,8 +39,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
@@ -218,14 +216,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
}

View File

@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_group_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateGroupAttribute;
convert_attribute_type!(create_group_attribute::AttributeType);
pub struct CreateGroupAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupAttributeModel>,
@@ -70,10 +68,11 @@ impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
invalid
);
})?;
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
let attribute_type =
serde_json::from_str::<AttributeType>(&model.attribute_type).unwrap();
let req = create_group_attribute::Variables {
name: model.attribute_name,
attribute_type: create_group_attribute::AttributeType::from(attribute_type),
attribute_type,
is_list: model.is_list,
is_visible: model.is_visible,
};

View File

@@ -7,7 +7,6 @@ use crate::{
},
router::AppRoute,
},
convert_attribute_type,
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -32,7 +31,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
@@ -40,8 +40,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
@@ -310,14 +308,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
}

View File

@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_user_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateUserAttribute;
convert_attribute_type!(create_user_attribute::AttributeType);
pub struct CreateUserAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserAttributeModel>,
@@ -74,10 +72,11 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
invalid
);
})?;
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
let attribute_type =
serde_json::from_str::<AttributeType>(&model.attribute_type).unwrap();
let req = create_user_attribute::Variables {
name: model.attribute_name,
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
attribute_type,
is_editable: model.is_editable,
is_list: model.is_list,
is_visible: model.is_visible,

View File

@@ -26,7 +26,7 @@ fn attribute_input(props: &AttributeInputProps) -> Html {
<DateTimeInput name={props.name.clone()} value={props.value.clone()} />
};
}
AttributeType::Jpeg => {
AttributeType::JpegPhoto => {
return html! {
<JpegFileInput name={props.name.clone()} value={props.value.clone()} />
};
@@ -82,7 +82,7 @@ fn attribute_label(props: &AttributeLabelProps) -> Html {
#[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps {
pub name: String,
pub attribute_type: AttributeType,
pub(crate) attribute_type: AttributeType,
#[prop_or(None)]
pub value: Option<String>,
}
@@ -94,7 +94,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
<AttributeLabel name={props.name.clone()} />
<div class="col-8">
<AttributeInput
attribute_type={props.attribute_type.clone()}
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.value.clone()} />
</div>
@@ -105,7 +105,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
#[derive(Properties, PartialEq)]
pub struct ListAttributeInputProps {
pub name: String,
pub attribute_type: AttributeType,
pub(crate) attribute_type: AttributeType,
#[prop_or(vec!())]
pub values: Vec<String>,
}
@@ -165,7 +165,7 @@ impl Component for ListAttributeInput {
{self.indices.iter().map(|&i| html! {
<div class="input-group mb-2" key={i}>
<AttributeInput
attribute_type={props.attribute_type.clone()}
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.values.get(i).cloned().unwrap_or_default()} />
<button

View File

@@ -5,10 +5,10 @@ use crate::{
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupDetails;
@@ -29,9 +30,6 @@ pub type User = get_group_details::GetGroupDetailsGroupUsers;
pub type AddGroupMemberUser = add_group_member::User;
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
pub type AttributeType = get_group_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {

View File

@@ -10,7 +10,6 @@ use crate::{
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
schema::AttributeType,
},
};
use anyhow::{Ok, Result};
@@ -174,7 +173,7 @@ fn get_custom_attribute_input(
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
@@ -182,7 +181,7 @@ fn get_custom_attribute_input(
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()}
/>
}

View File

@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::group,
common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
@@ -30,8 +30,6 @@ 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,
@@ -147,7 +145,7 @@ impl GroupSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
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>

View File

@@ -5,10 +5,10 @@ use crate::{
router::{AppRoute, Link},
user_details_form::UserDetailsForm,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserDetails;
@@ -28,9 +29,6 @@ pub type User = get_user_details::GetUserDetailsUser;
pub type Group = get_user_details::GetUserDetailsUserGroups;
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
pub type AttributeType = get_user_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {

View File

@@ -14,6 +14,7 @@ use crate::{
},
};
use anyhow::{Ok, Result};
use gloo_console::console;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -168,7 +169,7 @@ fn get_custom_attribute_input(
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
@@ -176,7 +177,7 @@ fn get_custom_attribute_input(
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()}
/>
}
@@ -192,9 +193,19 @@ fn get_custom_attribute_static(
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
let value_to_str = match attribute_schema.attribute_type {
AttributeType::String | AttributeType::Integer => |v: String| v,
AttributeType::DateTime => |v: String| {
console!(format!("Parsing date: {}", &v));
chrono::DateTime::parse_from_rfc3339(&v)
.map(|dt| dt.naive_utc().to_string())
.unwrap_or_else(|_| "Invalid date".to_string())
},
AttributeType::JpegPhoto => |_: String| "Unimplemented JPEG display".to_string(),
};
html! {
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()}
{values.into_iter().map(|x| html!{<div>{value_to_str(x)}</div>}).collect::<Vec<_>>()}
</StaticValue>
}
}

View File

@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::user,
common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
@@ -29,8 +29,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
@@ -146,7 +144,7 @@ impl UserSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
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>

View File

@@ -1,13 +1,14 @@
use anyhow::Result;
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use validator::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeType {
#[derive(Deserialize, Serialize, Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum AttributeType {
String,
Integer,
DateTime,
Jpeg,
JpegPhoto,
}
impl Display for AttributeType {
@@ -16,51 +17,8 @@ impl Display for AttributeType {
}
}
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 $crate::infra::schema::AttributeType {
fn from(value: $source_type) -> Self {
match value {
<$source_type>::STRING => $crate::infra::schema::AttributeType::String,
<$source_type>::INTEGER => $crate::infra::schema::AttributeType::Integer,
<$source_type>::DATE_TIME => $crate::infra::schema::AttributeType::DateTime,
<$source_type>::JPEG_PHOTO => $crate::infra::schema::AttributeType::Jpeg,
_ => panic!("Unknown attribute type"),
}
}
}
impl From<$crate::infra::schema::AttributeType> for $source_type {
fn from(value: $crate::infra::schema::AttributeType) -> Self {
match value {
$crate::infra::schema::AttributeType::String => <$source_type>::STRING,
$crate::infra::schema::AttributeType::Integer => <$source_type>::INTEGER,
$crate::infra::schema::AttributeType::DateTime => <$source_type>::DATE_TIME,
$crate::infra::schema::AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
}
}
}
};
}
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
AttributeType::from_str(attribute_type)
serde_json::from_str::<AttributeType>(attribute_type)
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
Ok(())
}