mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	app: Add support for modifying an avatar
This commit is contained in:
		
							parent
							
								
									b148fea008
								
							
						
					
					
						commit
						be6ba020c8
					
				
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -2159,6 +2159,7 @@ name = "lldap_app"
 | 
				
			|||||||
version = "0.4.0"
 | 
					version = "0.4.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 | 
					 "base64",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "graphql_client 0.10.0",
 | 
					 "graphql_client 0.10.0",
 | 
				
			||||||
 "http",
 | 
					 "http",
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ edition = "2021"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
anyhow = "1"
 | 
					anyhow = "1"
 | 
				
			||||||
 | 
					base64 = "0.13"
 | 
				
			||||||
graphql_client = "0.10"
 | 
					graphql_client = "0.10"
 | 
				
			||||||
http = "0.2"
 | 
					http = "0.2"
 | 
				
			||||||
jwt = "0.13"
 | 
					jwt = "0.13"
 | 
				
			||||||
@ -27,6 +28,7 @@ version = "0.3"
 | 
				
			|||||||
features = [
 | 
					features = [
 | 
				
			||||||
  "Document",
 | 
					  "Document",
 | 
				
			||||||
  "Element",
 | 
					  "Element",
 | 
				
			||||||
 | 
					  "FileReader",
 | 
				
			||||||
  "HtmlDocument",
 | 
					  "HtmlDocument",
 | 
				
			||||||
  "HtmlInputElement",
 | 
					  "HtmlInputElement",
 | 
				
			||||||
  "HtmlOptionElement",
 | 
					  "HtmlOptionElement",
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ query GetUserDetails($id: String!) {
 | 
				
			|||||||
    displayName
 | 
					    displayName
 | 
				
			||||||
    firstName
 | 
					    firstName
 | 
				
			||||||
    lastName
 | 
					    lastName
 | 
				
			||||||
 | 
					    avatar
 | 
				
			||||||
    creationDate
 | 
					    creationDate
 | 
				
			||||||
    uuid
 | 
					    uuid
 | 
				
			||||||
    groups {
 | 
					    groups {
 | 
				
			||||||
 | 
				
			|||||||
@ -198,8 +198,7 @@ impl Component for UserDetails {
 | 
				
			|||||||
                  <>
 | 
					                  <>
 | 
				
			||||||
                    <h3>{u.id.to_string()}</h3>
 | 
					                    <h3>{u.id.to_string()}</h3>
 | 
				
			||||||
                    <UserDetailsForm
 | 
					                    <UserDetailsForm
 | 
				
			||||||
                      user=u.clone()
 | 
					                      user=u.clone() />
 | 
				
			||||||
                      on_error=self.common.callback(Msg::OnError)/>
 | 
					 | 
				
			||||||
                    <div class="row justify-content-center">
 | 
					                    <div class="row justify-content-center">
 | 
				
			||||||
                      <NavButton
 | 
					                      <NavButton
 | 
				
			||||||
                        route=AppRoute::ChangePassword(u.id.clone())
 | 
					                        route=AppRoute::ChangePassword(u.id.clone())
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    components::user_details::User,
 | 
					    components::user_details::User,
 | 
				
			||||||
    infra::common_component::{CommonComponent, CommonComponentParts},
 | 
					    infra::common_component::{CommonComponent, CommonComponentParts},
 | 
				
			||||||
@ -5,9 +7,37 @@ use crate::{
 | 
				
			|||||||
use anyhow::{bail, Error, Result};
 | 
					use anyhow::{bail, Error, Result};
 | 
				
			||||||
use graphql_client::GraphQLQuery;
 | 
					use graphql_client::GraphQLQuery;
 | 
				
			||||||
use validator_derive::Validate;
 | 
					use validator_derive::Validate;
 | 
				
			||||||
use yew::prelude::*;
 | 
					use wasm_bindgen::JsCast;
 | 
				
			||||||
 | 
					use yew::{prelude::*, services::ConsoleService};
 | 
				
			||||||
use yew_form_derive::Model;
 | 
					use yew_form_derive::Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(PartialEq, Clone, Default)]
 | 
				
			||||||
 | 
					struct JsFile {
 | 
				
			||||||
 | 
					    file: Option<web_sys::File>,
 | 
				
			||||||
 | 
					    contents: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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<Self> {
 | 
				
			||||||
 | 
					        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.
 | 
					/// The fields of the form, with the editable details and the constraints.
 | 
				
			||||||
#[derive(Model, Validate, PartialEq, Clone)]
 | 
					#[derive(Model, Validate, PartialEq, Clone)]
 | 
				
			||||||
pub struct UserModel {
 | 
					pub struct UserModel {
 | 
				
			||||||
@ -34,6 +64,7 @@ pub struct UpdateUser;
 | 
				
			|||||||
pub struct UserDetailsForm {
 | 
					pub struct UserDetailsForm {
 | 
				
			||||||
    common: CommonComponentParts<Self>,
 | 
					    common: CommonComponentParts<Self>,
 | 
				
			||||||
    form: yew_form::Form<UserModel>,
 | 
					    form: yew_form::Form<UserModel>,
 | 
				
			||||||
 | 
					    avatar: JsFile,
 | 
				
			||||||
    /// True if we just successfully updated the user, to display a success message.
 | 
					    /// True if we just successfully updated the user, to display a success message.
 | 
				
			||||||
    just_updated: bool,
 | 
					    just_updated: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -43,6 +74,8 @@ pub enum Msg {
 | 
				
			|||||||
    Update,
 | 
					    Update,
 | 
				
			||||||
    /// The "Submit" button was clicked.
 | 
					    /// The "Submit" button was clicked.
 | 
				
			||||||
    SubmitClicked,
 | 
					    SubmitClicked,
 | 
				
			||||||
 | 
					    /// A picked file finished loading.
 | 
				
			||||||
 | 
					    FileLoaded(yew::services::reader::FileData),
 | 
				
			||||||
    /// We got the response from the server about our update message.
 | 
					    /// We got the response from the server about our update message.
 | 
				
			||||||
    UserUpdated(Result<update_user::ResponseData>),
 | 
					    UserUpdated(Result<update_user::ResponseData>),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -51,16 +84,58 @@ pub enum Msg {
 | 
				
			|||||||
pub struct Props {
 | 
					pub struct Props {
 | 
				
			||||||
    /// The current user details.
 | 
					    /// The current user details.
 | 
				
			||||||
    pub user: User,
 | 
					    pub user: User,
 | 
				
			||||||
    /// Callback to report errors (e.g. server error).
 | 
					 | 
				
			||||||
    pub on_error: Callback<Error>,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl CommonComponent<UserDetailsForm> for UserDetailsForm {
 | 
					impl CommonComponent<UserDetailsForm> for UserDetailsForm {
 | 
				
			||||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
					    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
				
			||||||
        match msg {
 | 
					        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::<web_sys::HtmlInputElement>()
 | 
				
			||||||
 | 
					                    .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::SubmitClicked => self.submit_user_update_form(),
 | 
				
			||||||
            Msg::UserUpdated(response) => self.user_update_finished(response),
 | 
					            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 {
 | 
					        Self {
 | 
				
			||||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
					            common: CommonComponentParts::<Self>::create(props, link),
 | 
				
			||||||
            form: yew_form::Form::new(model),
 | 
					            form: yew_form::Form::new(model),
 | 
				
			||||||
 | 
					            avatar: JsFile::default(),
 | 
				
			||||||
            just_updated: false,
 | 
					            just_updated: false,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
					    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
				
			||||||
        self.just_updated = false;
 | 
					        self.just_updated = false;
 | 
				
			||||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
					        CommonComponentParts::<Self>::update(self, msg)
 | 
				
			||||||
            self,
 | 
					 | 
				
			||||||
            msg,
 | 
					 | 
				
			||||||
            self.common.on_error.clone(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
					    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
				
			||||||
@ -102,6 +174,9 @@ impl Component for UserDetailsForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    fn view(&self) -> Html {
 | 
					    fn view(&self) -> Html {
 | 
				
			||||||
        type Field = yew_form::Field<UserModel>;
 | 
					        type Field = yew_form::Field<UserModel>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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! {
 | 
					        html! {
 | 
				
			||||||
          <div class="py-3">
 | 
					          <div class="py-3">
 | 
				
			||||||
            <form class="form">
 | 
					            <form class="form">
 | 
				
			||||||
@ -111,7 +186,24 @@ impl Component for UserDetailsForm {
 | 
				
			|||||||
                  {"User ID: "}
 | 
					                  {"User ID: "}
 | 
				
			||||||
                </label>
 | 
					                </label>
 | 
				
			||||||
                <div class="col-8">
 | 
					                <div class="col-8">
 | 
				
			||||||
                  <span id="userId" class="form-constrol-static">{&self.common.user.id}</span>
 | 
					                  <span id="userId" class="form-constrol-static"><b>{&self.common.user.id}</b></span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="form-group row mb-3">
 | 
				
			||||||
 | 
					                <div class="col-4 col-form-label">
 | 
				
			||||||
 | 
					                  <img
 | 
				
			||||||
 | 
					                    id="avatarDisplay"
 | 
				
			||||||
 | 
					                    src={format!("data:image/jpeg;base64, {}", avatar_string)}
 | 
				
			||||||
 | 
					                    style="max-height:128px;max-width:128px;height:auto;width:auto;"
 | 
				
			||||||
 | 
					                    alt="Avatar" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="col-8">
 | 
				
			||||||
 | 
					                  <input
 | 
				
			||||||
 | 
					                    class="form-control"
 | 
				
			||||||
 | 
					                    id="avatarInput"
 | 
				
			||||||
 | 
					                    type="file"
 | 
				
			||||||
 | 
					                    accept="image/jpeg"
 | 
				
			||||||
 | 
					                    oninput=self.common.callback(|_| Msg::Update) />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="form-group row mb-3">
 | 
					              <div class="form-group row mb-3">
 | 
				
			||||||
@ -214,6 +306,14 @@ impl Component for UserDetailsForm {
 | 
				
			|||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
 | 
					            { if let Some(e) = &self.common.error {
 | 
				
			||||||
 | 
					                html! {
 | 
				
			||||||
 | 
					                  <div class="alert alert-danger">
 | 
				
			||||||
 | 
					                    {e.to_string() }
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              } else { html! {} }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            <div hidden=!self.just_updated>
 | 
					            <div hidden=!self.just_updated>
 | 
				
			||||||
              <span>{"User successfully updated!"}</span>
 | 
					              <span>{"User successfully updated!"}</span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@ -224,9 +324,19 @@ impl Component for UserDetailsForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl UserDetailsForm {
 | 
					impl UserDetailsForm {
 | 
				
			||||||
    fn submit_user_update_form(&mut self) -> Result<bool> {
 | 
					    fn submit_user_update_form(&mut self) -> Result<bool> {
 | 
				
			||||||
 | 
					        ConsoleService::log("Submit");
 | 
				
			||||||
        if !self.form.validate() {
 | 
					        if !self.form.validate() {
 | 
				
			||||||
            bail!("Invalid inputs");
 | 
					            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 base_user = &self.common.user;
 | 
				
			||||||
        let mut user_input = update_user::UpdateUserInput {
 | 
					        let mut user_input = update_user::UpdateUserInput {
 | 
				
			||||||
            id: self.common.user.id.clone(),
 | 
					            id: self.common.user.id.clone(),
 | 
				
			||||||
@ -251,11 +361,14 @@ impl UserDetailsForm {
 | 
				
			|||||||
        if base_user.last_name != model.last_name {
 | 
					        if base_user.last_name != model.last_name {
 | 
				
			||||||
            user_input.lastName = Some(model.last_name);
 | 
					            user_input.lastName = Some(model.last_name);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        user_input.avatar = maybe_to_base64(&self.avatar)?;
 | 
				
			||||||
        // Nothing changed.
 | 
					        // Nothing changed.
 | 
				
			||||||
        if user_input == default_user_input {
 | 
					        if user_input == default_user_input {
 | 
				
			||||||
 | 
					            ConsoleService::log("No changes");
 | 
				
			||||||
            return Ok(false);
 | 
					            return Ok(false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let req = update_user::Variables { user: user_input };
 | 
					        let req = update_user::Variables { user: user_input };
 | 
				
			||||||
 | 
					        ConsoleService::log("Querying");
 | 
				
			||||||
        self.common.call_graphql::<UpdateUser, _>(
 | 
					        self.common.call_graphql::<UpdateUser, _>(
 | 
				
			||||||
            req,
 | 
					            req,
 | 
				
			||||||
            Msg::UserUpdated,
 | 
					            Msg::UserUpdated,
 | 
				
			||||||
@ -270,19 +383,44 @@ impl UserDetailsForm {
 | 
				
			|||||||
            Err(e) => return Err(e),
 | 
					            Err(e) => return Err(e),
 | 
				
			||||||
            Ok(_) => {
 | 
					            Ok(_) => {
 | 
				
			||||||
                let model = self.form.model();
 | 
					                let model = self.form.model();
 | 
				
			||||||
                self.common.user = User {
 | 
					                self.common.user.email = model.email;
 | 
				
			||||||
                    id: self.common.user.id.clone(),
 | 
					                self.common.user.display_name = model.display_name;
 | 
				
			||||||
                    email: model.email,
 | 
					                self.common.user.first_name = model.first_name;
 | 
				
			||||||
                    display_name: model.display_name,
 | 
					                self.common.user.last_name = model.last_name;
 | 
				
			||||||
                    first_name: model.first_name,
 | 
					                if let Some(avatar) = maybe_to_base64(&self.avatar)? {
 | 
				
			||||||
                    last_name: model.last_name,
 | 
					                    self.common.user.avatar = avatar;
 | 
				
			||||||
                    creation_date: self.common.user.creation_date,
 | 
					                }
 | 
				
			||||||
                    uuid: self.common.user.uuid.clone(),
 | 
					 | 
				
			||||||
                    groups: self.common.user.groups.clone(),
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                self.just_updated = true;
 | 
					                self.just_updated = true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        Ok(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<Option<String>> {
 | 
				
			||||||
 | 
					    match file {
 | 
				
			||||||
 | 
					        JsFile {
 | 
				
			||||||
 | 
					            file: None,
 | 
				
			||||||
 | 
					            contents: _,
 | 
				
			||||||
 | 
					        } => Ok(None),
 | 
				
			||||||
 | 
					        JsFile {
 | 
				
			||||||
 | 
					            file: Some(_),
 | 
				
			||||||
 | 
					            contents: None,
 | 
				
			||||||
 | 
					        } => bail!("Image file hasn't finished loading, try again"),
 | 
				
			||||||
 | 
					        JsFile {
 | 
				
			||||||
 | 
					            file: Some(_),
 | 
				
			||||||
 | 
					            contents: Some(data),
 | 
				
			||||||
 | 
					        } => {
 | 
				
			||||||
 | 
					            if !is_valid_jpeg(data.as_slice()) {
 | 
				
			||||||
 | 
					                bail!("Chosen image is not a valid JPEG");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(Some(base64::encode(data)))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,11 @@ use anyhow::{Error, Result};
 | 
				
			|||||||
use graphql_client::GraphQLQuery;
 | 
					use graphql_client::GraphQLQuery;
 | 
				
			||||||
use yew::{
 | 
					use yew::{
 | 
				
			||||||
    prelude::*,
 | 
					    prelude::*,
 | 
				
			||||||
    services::{fetch::FetchTask, ConsoleService},
 | 
					    services::{
 | 
				
			||||||
 | 
					        fetch::FetchTask,
 | 
				
			||||||
 | 
					        reader::{FileData, ReaderService, ReaderTask},
 | 
				
			||||||
 | 
					        ConsoleService,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use yewtil::NeqAssign;
 | 
					use yewtil::NeqAssign;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -40,13 +44,34 @@ pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
 | 
				
			|||||||
    fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
 | 
					    fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum AnyTask {
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					    FetchTask(FetchTask),
 | 
				
			||||||
 | 
					    ReaderTask(ReaderTask),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AnyTask {
 | 
				
			||||||
 | 
					    fn is_some(&self) -> bool {
 | 
				
			||||||
 | 
					        !matches!(self, AnyTask::None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<Option<FetchTask>> for AnyTask {
 | 
				
			||||||
 | 
					    fn from(task: Option<FetchTask>) -> Self {
 | 
				
			||||||
 | 
					        match task {
 | 
				
			||||||
 | 
					            Some(t) => AnyTask::FetchTask(t),
 | 
				
			||||||
 | 
					            None => AnyTask::None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Structure that contains the common parts needed by most components.
 | 
					/// Structure that contains the common parts needed by most components.
 | 
				
			||||||
/// The fields of [`props`] are directly accessible through a `Deref` implementation.
 | 
					/// The fields of [`props`] are directly accessible through a `Deref` implementation.
 | 
				
			||||||
pub struct CommonComponentParts<C: CommonComponent<C>> {
 | 
					pub struct CommonComponentParts<C: CommonComponent<C>> {
 | 
				
			||||||
    link: ComponentLink<C>,
 | 
					    link: ComponentLink<C>,
 | 
				
			||||||
    pub props: <C as Component>::Properties,
 | 
					    pub props: <C as Component>::Properties,
 | 
				
			||||||
    pub error: Option<Error>,
 | 
					    pub error: Option<Error>,
 | 
				
			||||||
    task: Option<FetchTask>,
 | 
					    task: AnyTask,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
					impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
				
			||||||
@ -57,7 +82,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// Cancel any background task.
 | 
					    /// Cancel any background task.
 | 
				
			||||||
    pub fn cancel_task(&mut self) {
 | 
					    pub fn cancel_task(&mut self) {
 | 
				
			||||||
        self.task = None;
 | 
					        self.task = AnyTask::None;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
 | 
					    pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
 | 
				
			||||||
@ -65,7 +90,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
				
			|||||||
            link,
 | 
					            link,
 | 
				
			||||||
            props,
 | 
					            props,
 | 
				
			||||||
            error: None,
 | 
					            error: None,
 | 
				
			||||||
            task: None,
 | 
					            task: AnyTask::None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +156,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
				
			|||||||
        M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
 | 
					        M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
 | 
				
			||||||
        Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
 | 
					        Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        self.task = Some(method(req, self.link.callback_once(callback))?);
 | 
					        self.task = AnyTask::FetchTask(method(req, self.link.callback_once(callback))?);
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -156,7 +181,19 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
				
			|||||||
            ConsoleService::log(&e.to_string());
 | 
					            ConsoleService::log(&e.to_string());
 | 
				
			||||||
            self.error = Some(e);
 | 
					            self.error = Some(e);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .ok();
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .into();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) fn read_file<Cb>(&mut self, file: web_sys::File, callback: Cb) -> Result<()>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        Cb: FnOnce(FileData) -> <C as Component>::Message + 'static,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self.task = AnyTask::ReaderTask(ReaderService::read_file(
 | 
				
			||||||
 | 
					            file,
 | 
				
			||||||
 | 
					            self.link.callback_once(callback),
 | 
				
			||||||
 | 
					        )?);
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user