From 52baa03d794f909ca2324061ef31cf019c1299ee Mon Sep 17 00:00:00 2001 From: Emilia Date: Mon, 5 Jun 2023 14:25:47 -0400 Subject: [PATCH] Export to CSV --- Cargo.lock | 23 ++++++ Cargo.toml | 6 +- .../scss/default_theme/components/_index.scss | 1 + .../components/_main_window.scss | 2 +- .../default_theme/components/_upper_menu.scss | 46 ++++++++++++ src/components/main_window.rs | 34 +++++++++ src/components/transfer_menu.rs | 2 +- src/data/csv.rs | 70 +++++++++++++++++++ src/data/mod.rs | 1 + 9 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 assets/scss/default_theme/components/_upper_menu.scss create mode 100644 src/data/csv.rs diff --git a/Cargo.lock b/Cargo.lock index 883e5ca..3395550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "csv" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.13.4" @@ -533,6 +554,8 @@ dependencies = [ name = "plate-tool" version = "0.1.0" dependencies = [ + "csv", + "js-sys", "lazy_static", "log", "regex", diff --git a/Cargo.toml b/Cargo.toml index 7a037b9..b80de30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,17 @@ edition = "2021" yew = { version = "0.20.0", features = ["csr"] } yewdux = "0.9" wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement", "HtmlDialogElement"] } +web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement", + "HtmlDialogElement", "Blob", "Url", "Window", + "HtmlAnchorElement"] } +js-sys = "0.3" log = "0.4" wasm-logger = "0.2" regex = "1" lazy_static = "1.4" uuid = { version = "1.3", features = ["v4", "fast-rng", "macro-diagnostics", "js", "serde"] } serde = { version = "1.0", features = ["derive"] } +csv = "1.2" [dev-dependencies] wasm-bindgen-test = "0.3.0" diff --git a/assets/scss/default_theme/components/_index.scss b/assets/scss/default_theme/components/_index.scss index bea7850..e0c2b5a 100644 --- a/assets/scss/default_theme/components/_index.scss +++ b/assets/scss/default_theme/components/_index.scss @@ -3,3 +3,4 @@ @forward "plates"; @forward "tree"; @forward "transfer_menu"; +@forward "upper_menu"; diff --git a/assets/scss/default_theme/components/_main_window.scss b/assets/scss/default_theme/components/_main_window.scss index b9b887c..4c33f6f 100644 --- a/assets/scss/default_theme/components/_main_window.scss +++ b/assets/scss/default_theme/components/_main_window.scss @@ -1,5 +1,5 @@ div.main_container { - height: 95vh; + height: 97vh; width: 98vw; margin-top: 2.5vh; margin-left: 1vw; diff --git a/assets/scss/default_theme/components/_upper_menu.scss b/assets/scss/default_theme/components/_upper_menu.scss new file mode 100644 index 0000000..9d812cf --- /dev/null +++ b/assets/scss/default_theme/components/_upper_menu.scss @@ -0,0 +1,46 @@ +@use "sass:color"; +@use "../variables" as *; + +div.upper_menu { + position: absolute; + top: 0px; + left: 0px; + + $menu-height: min(2.5vh, 25px); + height: $menu-height; + padding-left: 1vw; + + div.dropdown { + margin-right: 2px; + + position: relative; + height: $menu-height; + + display: flex; + flex-direction: column; + + outline: 1px solid $color-dark; + + button { + vertical-align: top; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + font-size: calc($menu-height*0.7); + } + + * { + visibility: hidden; + } + *:first-child { + visibility: visible; + } + + &:hover * { + visibility: visible; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; + } + } +} diff --git a/src/components/main_window.rs b/src/components/main_window.rs index d4f4cef..2a9cfaf 100644 --- a/src/components/main_window.rs +++ b/src/components/main_window.rs @@ -1,6 +1,9 @@ #![allow(non_snake_case)] use yew::prelude::*; use yewdux::prelude::*; +use wasm_bindgen::{JsValue, JsCast}; +use web_sys::{Blob, Url, HtmlAnchorElement}; +use js_sys::Array; use super::new_plate_dialog::NewPlateDialog; use super::plates::plate_container::PlateContainer; @@ -9,6 +12,7 @@ use super::transfer_menu::TransferMenu; use super::tree::Tree; use crate::data::plate_instances::PlateInstance; +use crate::data::csv::state_to_csv; #[function_component] pub fn MainWindow() -> Html { @@ -50,7 +54,36 @@ pub fn MainWindow() -> Html { }) }; + let save_button_callback = { + let main_state = main_state.clone(); + Callback::from(move |_| { + if let Ok(csv) = state_to_csv(&main_state) { + let csv: &str = &csv; + let blob = Blob::new_with_str_sequence( + &Array::from_iter(std::iter::once(JsValue::from_str(csv)))); + if let Ok(blob) = blob { + let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?"); + // Beneath is the cool hack to download files + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let anchor = document.create_element("a").unwrap() + .dyn_into::().unwrap(); + anchor.set_download("transfers.csv"); + anchor.set_href(&url); + anchor.click(); + } + } + }) + }; + html! { + <> +
+ +
@@ -60,5 +93,6 @@ pub fn MainWindow() -> Html { }
+ } } diff --git a/src/components/transfer_menu.rs b/src/components/transfer_menu.rs index c8fc567..8b077fc 100644 --- a/src/components/transfer_menu.rs +++ b/src/components/transfer_menu.rs @@ -365,7 +365,7 @@ fn letters_to_num(letters: &str) -> Option { } return Some(num); } -fn num_to_letters(num: u8) -> Option { +pub fn num_to_letters(num: u8) -> Option { if num == 0 { return None; } // Otherwise, we will not return none! diff --git a/src/data/csv.rs b/src/data/csv.rs new file mode 100644 index 0000000..094d078 --- /dev/null +++ b/src/data/csv.rs @@ -0,0 +1,70 @@ +use crate::data::transfer::Transfer; +use crate::components::transfer_menu::num_to_letters; +use crate::components::states::MainState; + +use std::{error::Error}; +use serde::Serialize; + +#[derive(Serialize, Debug)] +struct TransferRecord { + #[serde(rename = "Source Plate Barcode")] + source_plate: String, + #[serde(rename = "Source Well")] + source_well: String, + #[serde(rename = "Destination Plate Barcode")] + destination_plate: String, + #[serde(rename = "Destination Well")] + destination_well: String, + #[serde(rename = "Volume")] + volume: f64, + #[serde(rename = "Concentration")] + concentration: Option, +} + +pub fn state_to_csv(state: &MainState) -> Result> { + let mut records: Vec = Vec::new(); + for transfer in &state.transfers { + let src_barcode = state.source_plates.iter().find(|spi| spi.get_uuid() == transfer.source_id) + .ok_or("Found unpurged transfer")?; + let dest_barcode = state.destination_plates.iter().find(|dpi| dpi.get_uuid() == transfer.dest_id) + .ok_or("Found unpurged transfer")?; + records.append(&mut transfer_to_records(transfer, &src_barcode.name, &dest_barcode.name)) + } + return records_to_csv(records) +} + +fn transfer_to_records( + tr: &Transfer, + src_barcode: &str, + dest_barcode: &str, +) -> Vec { + let source_wells = tr.transfer_region.get_source_wells(); + let map = tr.transfer_region.calculate_map(); + + let mut records: Vec = vec![]; + + for s_well in source_wells { + let dest_wells = map(s_well); + if let Some(dest_wells) = dest_wells { + for d_well in dest_wells { + records.push(TransferRecord { + source_plate: src_barcode.to_string(), + source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1), + destination_plate: dest_barcode.to_string(), + destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1), + volume: 2.5, // Default value since not yet implemented + concentration: None }) + } + } + } + return records +} + +fn records_to_csv(trs: Vec) -> Result> { + let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); + for record in trs { + wtr.serialize(record)? + } + let data = String::from_utf8(wtr.into_inner()?)?; + return Ok(data) +} diff --git a/src/data/mod.rs b/src/data/mod.rs index 3654d37..5b695a1 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -2,3 +2,4 @@ pub mod plate; pub mod plate_instances; pub mod transfer; pub mod transfer_region; +pub mod csv;