mirror of https://github.com/lldap/lldap.git
Compare commits
5 Commits
fee6580d2c
...
47b4ae8207
Author | SHA1 | Date |
---|---|---|
Austin Alvarado | 47b4ae8207 | |
RobertL | 254a168e78 | |
Austin Alvarado | 5b817980a9 | |
Austin Alvarado | 66097f1880 | |
Austin Alvarado | adf3577f0e |
|
@ -37,12 +37,16 @@ version = "0.3"
|
|||
features = [
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"FileReader",
|
||||
"FormData",
|
||||
"HtmlDocument",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"HtmlOptionElement",
|
||||
"HtmlOptionsCollection",
|
||||
"HtmlSelectElement",
|
||||
"SubmitEvent",
|
||||
"console",
|
||||
]
|
||||
|
||||
|
|
|
@ -12,5 +12,17 @@ query GetUserDetails($id: String!) {
|
|||
id
|
||||
displayName
|
||||
}
|
||||
attributes {
|
||||
name
|
||||
value
|
||||
schema {
|
||||
name
|
||||
attributeType
|
||||
isList
|
||||
isVisible
|
||||
isEditable
|
||||
isHardcoded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod attribute_input;
|
||||
pub mod checkbox;
|
||||
pub mod field;
|
||||
pub mod select;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue