Perfect MediaPicker

This commit is contained in:
Franklin 2023-04-23 12:26:14 -04:00
parent fc2a2510ec
commit 89a7b99ed2
7 changed files with 137 additions and 14 deletions

20
Cargo.lock generated
View File

@ -795,6 +795,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1096,6 +1106,7 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -1575,6 +1586,15 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.11"

View File

@ -23,7 +23,7 @@ js-sys = "0.3"
web-sys = "0.3.61"
# other libs
reqwest = { version = "0.11.11", features = ["rustls-tls", "json", "blocking"], default-features = false }
reqwest = { version = "0.11.11", features = ["rustls-tls", "json", "blocking", "multipart"], default-features = false }
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde", "js"] }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }

View File

@ -5,12 +5,13 @@ use jl_types::{
dto::{
filters::Filter, listing::Listing, payloads::contact::ContactPayload,
project_card::ProjectCardDto,
item::Item,
},
};
use reqwest::Method;
use uuid::Uuid;
use super::base::perform_request_without_client;
use super::base::{perform_request_without_client, perform_multipart_request_without_client};
const BASE_URL: &str = "http://localhost:8095/";
@ -148,7 +149,16 @@ pub async fn create_new_contact_request(contact: ContactPayload) -> Result<(), e
)
.await
}
/*
pub async fn upload_image() -> Result<String, err::Error> {
perform_request_without_client(BASE_URL.into(), Method::POST, format!("images/"), Some(), 200, Vec::new(), None).await
}*/
pub async fn upload_image(item: Item, body: Vec<u8>) -> Result<String, err::Error> {
perform_multipart_request_without_client(
BASE_URL.into(),
Method::POST,
format!("admin/images/{item}"),
body,
200,
Vec::new(),
None,
)
.await
}

View File

@ -60,3 +60,59 @@ pub async fn perform_request_without_client<B: Serialize, R: DeserializeOwned>(
}
}
}
pub async fn perform_multipart_request_without_client<R: DeserializeOwned>(
base_url: String,
method: reqwest::Method,
path: String,
body: Vec<u8>,
expected_status_code: u16,
headers: Vec<(String, String)>,
params: Option<Vec<(String, String)>>,
) -> Result<R, Error> {
let client = Client::new();
let mut req_incomplete =
client.request(method, format!("{url}{path}", url = base_url, path = path));
for header in headers {
req_incomplete = req_incomplete.header(&header.0, &header.1);
}
if let Some(parameters) = params {
req_incomplete = req_incomplete.query(&parameters)
}
let part = reqwest::multipart::Part::bytes(body).file_name("image");
let form = reqwest::multipart::Form::new();
let form = form.part("image", part);
match req_incomplete.multipart(form).send().await {
// Error handling here
Ok(res) => {
// Request sent correctly
match res.status().as_u16() == expected_status_code {
true => {
match res.json::<R>().await {
Ok(resp_dto) => Ok(resp_dto), // Return correctly deserialized obj
Err(err) => Err(Error::Serde(MessageResource::from(err))),
}
}
false => {
//If status code is any other than expected
Err(Error::UnexpectedStatusCode(
expected_status_code,
res.status().as_u16(),
match res.json::<Vec<MessageResource>>().await {
Ok(messages) => messages,
Err(e) => vec![MessageResource::from(e)],
},
))
}
}
}
Err(e) => {
// Request couldn't be sent
Err(Error::Network(MessageResource::from(e)))
}
}
}

View File

@ -1,7 +1,9 @@
use jl_types::domain::media::{Media, MediaList};
use js_sys::Uint8Array;
use wasm_bindgen_futures::JsFuture;
use yew::prelude::*;
use crate::components::textfield::get_value_from_input_event;
use crate::{components::textfield::{get_value_from_input_event, get_files_from_input_event}, api};
#[function_component(MediaPicker)]
pub fn media_picker(props: &MediaPickerProps) -> Html {
@ -9,7 +11,7 @@ pub fn media_picker(props: &MediaPickerProps) -> Html {
<>
<div class={{"textfield-label-required"}}>{"Media del proyecto"}</div>
<div class={"mediapicker-container"}>
<MediaListRendered medialist={props.value.clone()} onchange={props.onchange.clone()}/>
<MediaListRendered medialist={props.value.clone()} onchange={props.onchange.clone()} item={props.item.clone()}/>
</div>
</>
}
@ -21,6 +23,7 @@ pub struct MediaPickerProps {
pub onchange: Option<Callback<String>>,
#[prop_or_default]
pub required: bool,
pub item: jl_types::dto::item::Item,
}
#[function_component(MediaListRendered)]
fn render_media_list(props: &MediaListProps) -> Html {
@ -90,15 +93,42 @@ fn render_media_list(props: &MediaListProps) -> Html {
let onphoto_upload = {
let media_handle = props.medialist.clone();
let onchange_cb = props.onchange.clone();
Callback::from(move |_: InputEvent| {
let item = props.item.clone();
Callback::from(move |e: InputEvent| {
match onchange_cb.clone() {
Some(cb) => cb.emit(String::new()),
None => {}
};
//TODO: Upload picture, then push url into medialist
let mut media = (*media_handle).clone();
media.media_list.push(Media::Photo("".into()));
media_handle.set(media);
let files_opt = get_files_from_input_event(e);
match files_opt {
Some(filelist) => {
match filelist.get(0) {
Some(file) => {
let media_handle = media_handle.clone();
let item = item.clone();
wasm_bindgen_futures::spawn_local(async move {
let array_buf = JsFuture::from(file.array_buffer()).await;
let array = Uint8Array::new(&array_buf.unwrap());
let bytes: Vec<u8> = array.to_vec();
// Upload to backend and retrieve url
match api::backend::upload_image(item, bytes).await {
Ok(url) => {
let mut media = (*media_handle).clone();
media.media_list.push(Media::Photo(url));
media_handle.set(media);
},
Err(error) => log::error!("Error uploading image to backend: {error}"),
};
});
},
None => {log::error!("No files in first element of filelist...")}
};
},
None => {
log::error!("Something weird happened. No files after selecting files")
}
}
})
};
@ -126,4 +156,5 @@ fn render_media_list(props: &MediaListProps) -> Html {
pub struct MediaListProps {
pub medialist: UseStateHandle<MediaList>,
pub onchange: Option<Callback<String>>,
pub item: jl_types::dto::item::Item,
}

View File

@ -79,3 +79,9 @@ pub fn get_value_from_textarea_event(e: InputEvent) -> String {
let target: HtmlTextAreaElement = event_target.dyn_into().unwrap_throw();
target.value()
}
pub fn get_files_from_input_event(e: InputEvent) -> Option<web_sys::FileList> {
let event: Event = e.dyn_into().unwrap_throw();
let event_target = event.target().unwrap_throw();
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
target.files()
}

View File

@ -228,7 +228,7 @@ pub fn generate_fields_for_project(props: &ProjectFieldsProps) -> Html {
<>
<TextField label={"Ciudad"} value={location_city} required={true} onchange={ontype_cb.clone()}/>
<TextField label={"Distrito"} value={location_district} required={true} onchange={ontype_cb.clone()} />
<MediaPicker value={media} onchange={ontype_cb.clone()}/>
<MediaPicker value={media} onchange={ontype_cb.clone()} item={jl_types::dto::item::Item::Project}/>
{if (*agent).clone().is_none() {
html! {
<div class={"textfield-container"}>