diff --git a/Cargo.lock b/Cargo.lock
index 72e6163..31524a1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2159,6 +2159,7 @@ name = "lldap_app"
version = "0.4.0"
dependencies = [
"anyhow",
+ "base64",
"chrono",
"graphql_client 0.10.0",
"http",
diff --git a/app/Cargo.toml b/app/Cargo.toml
index f02401f..5ff4197 100644
--- a/app/Cargo.toml
+++ b/app/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
anyhow = "1"
+base64 = "0.13"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
@@ -27,6 +28,7 @@ version = "0.3"
features = [
"Document",
"Element",
+ "FileReader",
"HtmlDocument",
"HtmlInputElement",
"HtmlOptionElement",
diff --git a/app/queries/get_user_details.graphql b/app/queries/get_user_details.graphql
index f476f2d..97370fb 100644
--- a/app/queries/get_user_details.graphql
+++ b/app/queries/get_user_details.graphql
@@ -5,6 +5,7 @@ query GetUserDetails($id: String!) {
displayName
firstName
lastName
+ avatar
creationDate
uuid
groups {
diff --git a/app/src/components/user_details.rs b/app/src/components/user_details.rs
index 2f57af0..67ab078 100644
--- a/app/src/components/user_details.rs
+++ b/app/src/components/user_details.rs
@@ -198,8 +198,7 @@ impl Component for UserDetails {
<>
{u.id.to_string()}
+ user=u.clone() />
,
+ contents: Option>,
+}
+
+impl ToString for JsFile {
+ fn to_string(&self) -> String {
+ self.file
+ .as_ref()
+ .map(web_sys::File::name)
+ .unwrap_or_else(String::new)
+ }
+}
+
+impl FromStr for JsFile {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result {
+ if s.is_empty() {
+ Ok(JsFile::default())
+ } else {
+ bail!("Building file from non-empty string")
+ }
+ }
+}
+
/// The fields of the form, with the editable details and the constraints.
#[derive(Model, Validate, PartialEq, Clone)]
pub struct UserModel {
@@ -34,6 +64,7 @@ pub struct UpdateUser;
pub struct UserDetailsForm {
common: CommonComponentParts,
form: yew_form::Form,
+ avatar: JsFile,
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
}
@@ -43,6 +74,8 @@ pub enum Msg {
Update,
/// The "Submit" button was clicked.
SubmitClicked,
+ /// A picked file finished loading.
+ FileLoaded(yew::services::reader::FileData),
/// We got the response from the server about our update message.
UserUpdated(Result),
}
@@ -51,16 +84,58 @@ pub enum Msg {
pub struct Props {
/// The current user details.
pub user: User,
- /// Callback to report errors (e.g. server error).
- pub on_error: Callback,
}
impl CommonComponent for UserDetailsForm {
fn handle_msg(&mut self, msg: ::Message) -> Result {
match msg {
- Msg::Update => Ok(true),
+ Msg::Update => {
+ let window = web_sys::window().expect("no global `window` exists");
+ let document = window.document().expect("should have a document on window");
+ let input = document
+ .get_element_by_id("avatarInput")
+ .expect("Form field avatarInput should be present")
+ .dyn_into::()
+ .expect("Should be an HtmlInputElement");
+ ConsoleService::log("Form update");
+ if let Some(files) = input.files() {
+ ConsoleService::log("Got file list");
+ if files.length() > 0 {
+ ConsoleService::log("Got a file");
+ let new_avatar = JsFile {
+ file: files.item(0),
+ contents: None,
+ };
+ if self.avatar.file.as_ref().map(|f| f.name())
+ != new_avatar.file.as_ref().map(|f| f.name())
+ {
+ if let Some(ref file) = new_avatar.file {
+ self.mut_common().read_file(file.clone(), Msg::FileLoaded)?;
+ }
+ self.avatar = new_avatar;
+ }
+ }
+ }
+ Ok(true)
+ }
Msg::SubmitClicked => self.submit_user_update_form(),
Msg::UserUpdated(response) => self.user_update_finished(response),
+ Msg::FileLoaded(data) => {
+ self.common.cancel_task();
+ if let Some(file) = &self.avatar.file {
+ if file.name() == data.name {
+ if !is_valid_jpeg(data.content.as_slice()) {
+ // Clear the selection.
+ self.avatar = JsFile::default();
+ bail!("Chosen image is not a valid JPEG");
+ } else {
+ self.avatar.contents = Some(data.content);
+ return Ok(true);
+ }
+ }
+ }
+ Ok(false)
+ }
}
}
@@ -83,17 +158,14 @@ impl Component for UserDetailsForm {
Self {
common: CommonComponentParts::::create(props, link),
form: yew_form::Form::new(model),
+ avatar: JsFile::default(),
just_updated: false,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
self.just_updated = false;
- CommonComponentParts::::update_and_report_error(
- self,
- msg,
- self.common.on_error.clone(),
- )
+ CommonComponentParts::::update(self, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
@@ -102,6 +174,9 @@ impl Component for UserDetailsForm {
fn view(&self) -> Html {
type Field = yew_form::Field;
+
+ let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default();
+ let avatar_string = avatar_base64.as_ref().unwrap_or(&self.common.user.avatar);
html! {
+
@@ -214,6 +306,14 @@ impl Component for UserDetailsForm {
+ { if let Some(e) = &self.common.error {
+ html! {
+
+ {e.to_string() }
+
+ }
+ } else { html! {} }
+ }
{"User successfully updated!"}
@@ -224,9 +324,19 @@ impl Component for UserDetailsForm {
impl UserDetailsForm {
fn submit_user_update_form(&mut self) -> Result {
+ ConsoleService::log("Submit");
if !self.form.validate() {
bail!("Invalid inputs");
}
+ ConsoleService::log("Valid inputs");
+ if let JsFile {
+ file: Some(_),
+ contents: None,
+ } = &self.avatar
+ {
+ bail!("Image file hasn't finished loading, try again");
+ }
+ ConsoleService::log("File is correctly loaded");
let base_user = &self.common.user;
let mut user_input = update_user::UpdateUserInput {
id: self.common.user.id.clone(),
@@ -251,11 +361,14 @@ impl UserDetailsForm {
if base_user.last_name != model.last_name {
user_input.lastName = Some(model.last_name);
}
+ user_input.avatar = maybe_to_base64(&self.avatar)?;
// Nothing changed.
if user_input == default_user_input {
+ ConsoleService::log("No changes");
return Ok(false);
}
let req = update_user::Variables { user: user_input };
+ ConsoleService::log("Querying");
self.common.call_graphql::(
req,
Msg::UserUpdated,
@@ -270,19 +383,44 @@ impl UserDetailsForm {
Err(e) => return Err(e),
Ok(_) => {
let model = self.form.model();
- self.common.user = User {
- id: self.common.user.id.clone(),
- email: model.email,
- display_name: model.display_name,
- first_name: model.first_name,
- last_name: model.last_name,
- creation_date: self.common.user.creation_date,
- uuid: self.common.user.uuid.clone(),
- groups: self.common.user.groups.clone(),
- };
+ self.common.user.email = model.email;
+ self.common.user.display_name = model.display_name;
+ self.common.user.first_name = model.first_name;
+ self.common.user.last_name = model.last_name;
+ if let Some(avatar) = maybe_to_base64(&self.avatar)? {
+ self.common.user.avatar = avatar;
+ }
self.just_updated = true;
}
};
Ok(true)
}
}
+
+fn is_valid_jpeg(bytes: &[u8]) -> bool {
+ image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
+ .decode()
+ .is_ok()
+}
+
+fn maybe_to_base64(file: &JsFile) -> Result