Compare commits

...

5 Commits

Author SHA1 Message Date
Austin Alvarado 47b4ae8207
Merge 5b817980a9 into 254a168e78 2024-05-04 00:37:09 +02:00
RobertL 254a168e78
example_configs: mailserver: Include protocol in server host definition
Without the protocol specified, Mailserver throws an error
2024-05-03 09:32:54 +02:00
Austin Alvarado 5b817980a9 test point 2024-02-09 06:44:11 +00:00
Austin Alvarado 66097f1880 Merge branch 'main' into user-attribute-form 2024-02-09 05:37:50 +00:00
Austin Alvarado adf3577f0e commit so i can pull in fixes from master 2024-02-09 05:31:46 +00:00
8 changed files with 190 additions and 14 deletions

View File

@ -37,12 +37,16 @@ version = "0.3"
features = [
"Document",
"Element",
"Event",
"FileReader",
"FormData",
"HtmlDocument",
"HtmlFormElement",
"HtmlInputElement",
"HtmlOptionElement",
"HtmlOptionsCollection",
"HtmlSelectElement",
"SubmitEvent",
"console",
]

View File

@ -12,5 +12,17 @@ query GetUserDetails($id: String!) {
id
displayName
}
attributes {
name
value
schema {
name
attributeType
isList
isVisible
isEditable
isHardcoded
}
}
}
}

View File

@ -0,0 +1,70 @@
use crate::infra::schema::AttributeType;
use yew::{
function_component, html, virtual_dom::AttrValue, Callback, InputEvent, NodeRef, Properties,
};
/*
<input
ref={&ctx.props().input_ref}
type="text"
class="input-component"
placeholder={placeholder}
onmouseover={ctx.link().callback(|_| Msg::Hover)}
/>
*/
#[derive(Properties, PartialEq)]
struct AttributeInputProps {
name: AttrValue,
attribute_type: AttributeType,
#[prop_or(None)]
value: Option<String>,
}
#[function_component(AttributeInput)]
fn attribute_input(props: &AttributeInputProps) -> Html {
let input_type = match props.attribute_type {
AttributeType::String => "text",
AttributeType::Integer => "number",
AttributeType::DateTime => "datetime-local",
AttributeType::Jpeg => "file",
};
let accept = match props.attribute_type {
AttributeType::Jpeg => Some("image/jpeg"),
_ => None,
};
html! {
<input
type={input_type}
accept={accept}
name={props.name.clone()}
class="form-control"
value={props.value.clone()} />
}
}
#[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps {
pub name: String,
pub attribute_type: AttributeType,
#[prop_or(None)]
pub value: Option<String>,
}
#[function_component(SingleAttributeInput)]
pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
html! {
<div class="row mb-3">
<label for={props.name.clone()}
class="form-label col-4 col-form-label">
{&props.name}{":"}
</label>
<div class="col-8">
<AttributeInput
attribute_type={props.attribute_type.clone()}
name={props.name.clone()}
value={props.value.clone()} />
</div>
</div>
}
}

View File

@ -1,3 +1,4 @@
pub mod attribute_input;
pub mod checkbox;
pub mod field;
pub mod select;

View File

@ -4,8 +4,8 @@ use crate::{
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link},
user_details_form::UserDetailsForm,
},
infra::common_component::{CommonComponent, CommonComponentParts},
}, infra::{schema::AttributeType, common_component::{CommonComponent, CommonComponentParts}},
convert_attribute_type
};
use anyhow::{bail, Error, Result};
use graphql_client::GraphQLQuery;
@ -22,6 +22,10 @@ pub struct GetUserDetails;
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::GetUserDetailsUserAttributesSchema;
convert_attribute_type!(get_user_details::AttributeType);
pub struct UserDetails {
common: CommonComponentParts<Self>,

View File

@ -2,22 +2,25 @@ use std::str::FromStr;
use crate::{
components::{
form::{field::Field, static_value::StaticValue, submit::Submit},
user_details::User,
},
infra::common_component::{CommonComponent, CommonComponentParts},
form::{attribute_input::SingleAttributeInput, field::Field, static_value::StaticValue, submit::Submit},
user_details::{AttributeSchema, User},
}, convert_attribute_type, infra::{common_component::{CommonComponent, CommonComponentParts}, schema::AttributeType}
};
use anyhow::{bail, Error, Result};
use anyhow::{anyhow, bail, Error, Ok, Result};
use gloo_console::log;
use gloo_file::{
callbacks::{read_as_bytes, FileReader},
File,
};
use graphql_client::GraphQLQuery;
use validator::HasLen;
use validator_derive::Validate;
use web_sys::{FileList, HtmlInputElement, InputEvent};
use web_sys::{FileList, FormData, HtmlFormElement, HtmlInputElement, InputEvent};
use yew::prelude::*;
use yew_form_derive::Model;
use super::user_details::Attribute;
#[derive(Default)]
struct JsFile {
file: Option<File>,
@ -73,6 +76,7 @@ pub struct UserDetailsForm {
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
user: User,
form_ref: NodeRef,
}
pub enum Msg {
@ -150,7 +154,14 @@ impl CommonComponent<UserDetailsForm> for UserDetailsForm {
}
self.reader = None;
Ok(false)
}
} // Msg::OnSubmit(e) => {
// e.prevent_default();
// let form: HtmlFormElement = e.target_unchecked_into();
// let data = FormData::new_with_form(&form).unwrap();
// log!(format!("form data{:#?}", data));
// log!(format!("form data data{:#?}", *data));
// Ok(true)
// }
}
}
@ -177,6 +188,7 @@ impl Component for UserDetailsForm {
just_updated: false,
reader: None,
user: ctx.props().user.clone(),
form_ref: NodeRef::default(),
}
}
@ -275,10 +287,11 @@ impl Component for UserDetailsForm {
</div>
</div>
</div>
{self.user.attributes.iter().map(get_custom_attribute_input).collect::<Vec<_>>()}
<Submit
text="Save changes"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})} />
onclick={link.callback(|e: MouseEvent| {Msg::SubmitClicked})} />
</form>
{
if let Some(e) = &self.common.error {
@ -297,6 +310,45 @@ impl Component for UserDetailsForm {
}
}
type AttributeValue = (String, Vec<String>);
fn get_values_from_form_data(
schema: Vec<AttributeSchema>,
form: &FormData,
) -> Result<Vec<AttributeValue>> {
schema
.into_iter()
.map(|attr| -> Result<AttributeValue> {
let val = form
.get_all(attr.name.as_str())
.iter()
.map(|js_val| js_val.as_string().unwrap())
.filter(|val| !val.is_empty())
.collect::<Vec<String>>();
if val.length() > 1 && !attr.is_list {
return Err(anyhow!(
"Multiple values supplied for non-list attribute {}",
attr.name
));
}
Ok((attr.name.clone(), val))
})
.collect()
}
fn get_custom_attribute_input(attribute: &Attribute) -> Html {
if attribute.schema.is_list {
html!{<p>{"list attr"}</p>}
} else {
let value = if attribute.value.is_empty() {
None
} else {
Some(attribute.value[0].clone())
};
html!{<SingleAttributeInput name={attribute.name.clone()} attribute_type={Into::<AttributeType>::into(attribute.schema.attribute_type.clone())} value={value}/>}
}
}
impl UserDetailsForm {
fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
if !self.form.validate() {
@ -309,7 +361,40 @@ impl UserDetailsForm {
{
bail!("Image file hasn't finished loading, try again");
}
let form = self.form_ref.cast::<HtmlFormElement>().unwrap();
let form_data = FormData::new_with_form(&form)
.map_err(|e| anyhow!("Failed to get FormData: {:#?}", e.as_string()))?;
let mut all_values = get_values_from_form_data(
self.user
.attributes
.iter()
.map(|attr| attr.schema.clone())
.filter(|attr| !attr.is_hardcoded)
.filter(|attr| attr.is_editable)
.collect(),
&form_data,
)?;
let base_user = &self.user;
let base_attrs = &self.user.attributes;
all_values.retain(|(name, val)| {
let name = name.clone();
let base_val = base_attrs
.into_iter()
.find(|base_val| base_val.name == name)
.unwrap();
let new_values = val.clone();
base_val.value != new_values
});
let remove_names: Option<Vec<String>> = if all_values.is_empty() {
None
} else {
Some(all_values.iter().map(|(name, _)| name.clone()).collect())
};
let insert_attrs: Option<Vec<update_user::AttributeValueInput>> = if remove_names.is_none() {
None
} else {
Some(all_values.into_iter().map(|(name, value)| update_user::AttributeValueInput{name, value}).collect())
};
let mut user_input = update_user::UpdateUserInput {
id: self.user.id.clone(),
email: None,
@ -317,8 +402,8 @@ impl UserDetailsForm {
firstName: None,
lastName: None,
avatar: None,
removeAttributes: None,
insertAttributes: None,
removeAttributes: remove_names,
insertAttributes: insert_attrs,
};
let default_user_input = user_input.clone();
let model = self.form.model();

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use std::{fmt::Display, str::FromStr};
use validator::ValidationError;
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeType {
String,
Integer,

View File

@ -54,7 +54,7 @@ services:
- ENABLE_OPENDMARC=0
# >>> Postfix LDAP Integration
- ACCOUNT_PROVISIONER=LDAP
- LDAP_SERVER_HOST=lldap:3890
- LDAP_SERVER_HOST=ldap://lldap:3890
- LDAP_SEARCH_BASE=ou=people,dc=example,dc=com
- LDAP_BIND_DN=uid=admin,ou=people,dc=example,dc=com
- LDAP_BIND_PW=adminpassword