From 56758751f11b3c560208c6b725c982a566ba4c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Huvar?= <492849@mail.muni.cz> Date: Sat, 5 Oct 2024 21:58:20 +0200 Subject: [PATCH] Refactor datasets/observations on FE and BE, add new events, fix bugs... --- data/real_cases/arabidopsis_via_dataset.json | 3 + data/simple_sketch.json | 6 + data/test_data/test_model_with_data.json | 6 + .../eval_dynamic/processed_props.rs | 2 +- .../sketchbook/_tests_events/_observations.rs | 4 +- .../sketchbook/data_structs/_dataset_data.rs | 10 +- .../data_structs/_observation_data.rs | 7 +- .../observations/_dataset/_impl_dataset.rs | 76 +++++++--- .../observations/_dataset/_impl_events.rs | 23 ++- .../observations/_dataset/_impl_serde.rs | 16 +- .../sketchbook/observations/_dataset/mod.rs | 8 +- .../_manager/_impl_load_dataset.rs | 9 +- .../observations/_manager/_impl_manager.rs | 47 +++++- .../_manager/_impl_session_state.rs | 47 +++++- .../sketchbook/observations/_observation.rs | 46 +++++- src/aeon_events.ts | 40 ++++- .../observations-editor.less | 30 +++- .../observations-editor.ts | 142 ++++++++++++++---- .../observations-import.ts | 13 +- .../observations-set/observations-set.ts | 49 +++++- .../observations-editor/tabulator-utility.ts | 30 +++- .../root-component/root-component.ts | 3 +- src/html/util/data-interfaces.ts | 1 + 23 files changed, 505 insertions(+), 113 deletions(-) diff --git a/data/real_cases/arabidopsis_via_dataset.json b/data/real_cases/arabidopsis_via_dataset.json index 84642f1..0141ff7 100644 --- a/data/real_cases/arabidopsis_via_dataset.json +++ b/data/real_cases/arabidopsis_via_dataset.json @@ -440,14 +440,17 @@ "datasets": [ { "id": "dataset_1", + "name": "dataset_1", "observations": [ { "id": "Observation1", + "name": "Observation1", "dataset": "dataset_1", "values": "100110011111001001000" }, { "id": "Observation2", + "name": "Observation2", "dataset": "dataset_1", "values": "011101100001110111111" } diff --git a/data/simple_sketch.json b/data/simple_sketch.json index 097bbd0..669df4f 100644 --- a/data/simple_sketch.json +++ b/data/simple_sketch.json @@ -292,9 +292,11 @@ "datasets": [ { "id": "dataset1", + "name": "dataset1", "observations": [ { "id": "Observation1", + "name": "Observation1", "dataset": "dataset1", "values": "101010101" } @@ -313,19 +315,23 @@ }, { "id": "dataset0", + "name": "dataset0", "observations": [ { "id": "Observation1", + "name": "Observation1", "dataset": "dataset0", "values": "101010101" }, { "id": "Observation2", + "name": "Observation2", "dataset": "dataset0", "values": "010101010" }, { "id": "Observation3", + "name": "Observation3", "dataset": "dataset0", "values": "101010101" } diff --git a/data/test_data/test_model_with_data.json b/data/test_data/test_model_with_data.json index 2f0ef61..6ed2529 100644 --- a/data/test_data/test_model_with_data.json +++ b/data/test_data/test_model_with_data.json @@ -145,14 +145,17 @@ "datasets": [ { "id": "data_mts", + "name": "data_mts", "observations": [ { "id": "abc", + "name": "abc", "dataset": "data_mts", "values": "111*" }, { "id": "ab", + "name": "ab", "dataset": "data_mts", "values": "11**" } @@ -166,14 +169,17 @@ }, { "id": "data_fp", + "name": "data_fp", "observations": [ { "id": "ones", + "name": "ones", "dataset": "data_fp", "values": "1111" }, { "id": "zeros", + "name": "zeros", "dataset": "data_fp", "values": "0000" } diff --git a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs index eb25b82..9313c50 100644 --- a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs +++ b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs @@ -122,7 +122,7 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S let observation = dataset.get_obs(obs_id)?.clone(); let var_names = dataset.variable_names(); let var_names_ref = var_names.iter().map(|v| v.as_str()).collect(); - dataset = Dataset::new(vec![observation], var_names_ref)?; + dataset = Dataset::new("trap_space_data", vec![observation], var_names_ref)?; } ProcessedDynProp::mk_trap_space( diff --git a/src-tauri/src/sketchbook/_tests_events/_observations.rs b/src-tauri/src/sketchbook/_tests_events/_observations.rs index 6eccd05..3ecd3fe 100644 --- a/src-tauri/src/sketchbook/_tests_events/_observations.rs +++ b/src-tauri/src/sketchbook/_tests_events/_observations.rs @@ -12,7 +12,7 @@ fn prepare_dataset_3v_2o() -> Dataset { let obs2 = Observation::try_from_str("000", "o2").unwrap(); let obs_list = vec![obs1, obs2]; let var_names = vec!["a", "b", "c"]; - Dataset::new(obs_list.clone(), var_names.clone()).unwrap() + Dataset::new("dataset_3v_2o", obs_list.clone(), var_names.clone()).unwrap() } /// Prepare a simple dataset with 2 variables and 1 observation. @@ -20,7 +20,7 @@ fn prepare_dataset_2v_1o() -> Dataset { let obs1 = Observation::try_from_str("11", "o1").unwrap(); let obs_list = vec![obs1]; let var_names = vec!["v1", "v2"]; - Dataset::new(obs_list.clone(), var_names.clone()).unwrap() + Dataset::new("dataset_2v_1o", obs_list.clone(), var_names.clone()).unwrap() } #[test] diff --git a/src-tauri/src/sketchbook/data_structs/_dataset_data.rs b/src-tauri/src/sketchbook/data_structs/_dataset_data.rs index 05b660a..2d55102 100644 --- a/src-tauri/src/sketchbook/data_structs/_dataset_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_dataset_data.rs @@ -10,18 +10,20 @@ use serde::{Deserialize, Serialize}; /// instead of more complex typesafe structs) to allow for easier (de)serialization. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DatasetData { + pub name: String, pub id: String, pub observations: Vec, pub variables: Vec, } -/// Structure for sending *metadata* about `Dataset`. This includes id, variable names, +/// Structure for sending *metadata* about `Dataset`. This includes name, id, variable names, /// but excludes all observations. /// /// Some fields simplified compared to original typesafe versions (e.g., pure `Strings` are used /// instead of more complex typesafe structs) to allow for easier (de)serialization. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DatasetMetaData { + pub name: String, pub id: String, pub variables: Vec, } @@ -39,6 +41,7 @@ impl DatasetData { .collect(); let variables = dataset.variables().iter().map(|v| v.to_string()).collect(); DatasetData { + name: dataset.get_name().to_string(), id: id.to_string(), observations, variables, @@ -54,7 +57,7 @@ impl DatasetData { .map(|o| o.to_observation()) .collect::, String>>()?; let variables = self.variables.iter().map(|v| v.as_str()).collect(); - Dataset::new(observations, variables) + Dataset::new(self.name.as_str(), observations, variables) } } @@ -63,6 +66,7 @@ impl DatasetMetaData { pub fn from_dataset(id: &DatasetId, dataset: &Dataset) -> DatasetMetaData { let variables = dataset.variables().iter().map(|v| v.to_string()).collect(); DatasetMetaData { + name: dataset.get_name().to_string(), id: id.to_string(), variables, } @@ -81,7 +85,7 @@ mod tests { let dataset_id = DatasetId::new("d").unwrap(); let obs1 = Observation::try_from_str("*1", "o1").unwrap(); let obs2 = Observation::try_from_str("00", "o2").unwrap(); - let dataset_before = Dataset::new(vec![obs1, obs2], vec!["a", "b"]).unwrap(); + let dataset_before = Dataset::new("d", vec![obs1, obs2], vec!["a", "b"]).unwrap(); let dataset_data = DatasetData::from_dataset(&dataset_id, &dataset_before); let dataset_after = dataset_data.to_dataset().unwrap(); diff --git a/src-tauri/src/sketchbook/data_structs/_observation_data.rs b/src-tauri/src/sketchbook/data_structs/_observation_data.rs index ecd7c7f..38efa6a 100644 --- a/src-tauri/src/sketchbook/data_structs/_observation_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_observation_data.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ObservationData { pub id: String, + pub name: String, pub dataset: String, pub values: String, } @@ -19,9 +20,10 @@ impl<'de> JsonSerde<'de> for ObservationData {} impl ObservationData { /// Create new `ObservationData` instance given `id` and values string slices. - pub fn new(obs_id: &str, dataset_id: &str, values: &str) -> ObservationData { + pub fn new(obs_id: &str, name: &str, dataset_id: &str, values: &str) -> ObservationData { ObservationData { id: obs_id.to_string(), + name: name.to_string(), dataset: dataset_id.to_string(), values: values.to_string(), } @@ -32,6 +34,7 @@ impl ObservationData { pub fn from_obs(obs: &Observation, dataset_id: &DatasetId) -> ObservationData { ObservationData::new( obs.get_id().as_str(), + obs.get_name(), dataset_id.as_str(), &obs.to_values_string(), ) @@ -40,7 +43,7 @@ impl ObservationData { /// Extract the corresponding `Observation` from the `ObservationData`. /// There is a syntax check just to make sure that the data are valid. pub fn to_observation(&self) -> Result { - Observation::try_from_str(&self.values.clone(), &self.id) + Observation::try_from_str_named(&self.values.clone(), &self.id, &self.name) } } diff --git a/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs b/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs index 5c99742..140a197 100644 --- a/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs +++ b/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs @@ -1,6 +1,6 @@ use crate::sketchbook::ids::{ObservationId, VarId}; use crate::sketchbook::observations::{Dataset, Observation, VarValue}; -use crate::sketchbook::utils::assert_ids_unique; +use crate::sketchbook::utils::{assert_ids_unique, assert_name_valid}; use std::collections::HashMap; /// Creating new `Dataset` instances. @@ -9,7 +9,13 @@ impl Dataset { /// /// Length of each observation and number of variables must match. /// Observation IDs must be valid identifiers and must be unique. - pub fn new(observations: Vec, var_names: Vec<&str>) -> Result { + pub fn new( + name: &str, + observations: Vec, + var_names: Vec<&str>, + ) -> Result { + assert_name_valid(name)?; + // check that all variables are unique and valid, same for observation IDs let variables = Self::try_convert_vars(&var_names)?; assert_ids_unique(&variables)?; @@ -30,6 +36,7 @@ impl Dataset { } Ok(Self { + name: name.to_string(), observations, variables, index_map, @@ -37,8 +44,13 @@ impl Dataset { } /// Shorthand to create new `empty` dataset over given variables. - pub fn new_empty(var_names: Vec<&str>) -> Result { - Self::new(Vec::new(), var_names) + pub fn new_empty(name: &str, var_names: Vec<&str>) -> Result { + Self::new(name, Vec::new(), var_names) + } + + /// Default dataset instance with no Variables or Observations. + pub fn default(name: &str) -> Dataset { + Dataset::new_empty(name, Vec::new()).unwrap() } /// **(internal)** Try converting variables string slices into VarIDs. @@ -52,6 +64,13 @@ impl Dataset { /// Editing `Dataset` instances. impl Dataset { + /// Set dataset's name. + pub fn set_name(&mut self, name: &str) -> Result<(), String> { + assert_name_valid(name)?; + self.name = name.to_string(); + Ok(()) + } + /// Add observation at the end of the dataset. /// /// The observation must have the same length as is the number of dataset's variables, and its @@ -132,8 +151,8 @@ impl Dataset { /// placeholders. pub fn add_var_default(&mut self, var_id: VarId, index: usize) -> Result<(), String> { self.assert_no_variable(&var_id)?; - if index > self.num_observations() { - return Err("Index is larger than number of observations.".to_string()); + if index > self.num_variables() { + return Err("Index is larger than number of variables.".to_string()); } self.variables.insert(index, var_id); @@ -206,10 +225,21 @@ impl Dataset { let new_id = ObservationId::new(new_id)?; self.set_obs_id(&original_id, new_id) } + + /// Set name of a given observation. + pub fn set_obs_name(&mut self, id: &ObservationId, new_name: &str) -> Result<(), String> { + let idx = self.get_obs_index(id)?; + self.observations[idx].set_name(new_name) + } } /// Observing `Dataset` instances. impl Dataset { + /// Number of variables tracked by the dataset. + pub fn get_name(&self) -> &str { + &self.name + } + /// Number of observations in the dataset. pub fn num_observations(&self) -> usize { self.observations.len() @@ -385,16 +415,17 @@ mod tests { #[test] /// Test that valid datasets are created correctly. fn test_new_dataset() { + let name = "dataset"; let obs1 = Observation::try_from_str("*11", "i1").unwrap(); let obs2 = Observation::try_from_str("000", "i2").unwrap(); let obs_list = vec![obs1, obs2]; let var_names = vec!["a", "b", "c"]; - let dataset = Dataset::new_empty(var_names.clone()).unwrap(); + let dataset = Dataset::new_empty(name, var_names.clone()).unwrap(); assert_eq!(dataset.num_observations(), 0); assert_eq!(dataset.num_variables(), 3); - let dataset = Dataset::new(obs_list.clone(), var_names.clone()).unwrap(); + let dataset = Dataset::new(name, obs_list.clone(), var_names.clone()).unwrap(); assert_eq!(dataset.num_observations(), 2); assert_eq!(dataset.num_variables(), 3); } @@ -402,34 +433,36 @@ mod tests { #[test] /// Test that invalid datasets cannot be created. fn test_invalid_dataset() { + let name = "dataset"; let obs1 = Observation::try_from_str("*1", "i1").unwrap(); let obs2 = Observation::try_from_str("000", "i2").unwrap(); let var_names = vec!["a", "b"]; // two cases where length of observation and number variables differs let observations = vec![obs2.clone()]; - let obs_list = Dataset::new(observations, var_names.clone()); + let obs_list = Dataset::new(name, observations, var_names.clone()); assert!(obs_list.is_err()); let observations = vec![obs1.clone(), obs2.clone()]; - let obs_list = Dataset::new(observations, var_names.clone()); + let obs_list = Dataset::new(name, observations, var_names.clone()); assert!(obs_list.is_err()); // trying to add observation with the same id twice let observations = vec![obs1.clone(), obs1.clone()]; - let obs_list = Dataset::new(observations, var_names.clone()); + let obs_list = Dataset::new(name, observations, var_names.clone()); assert!(obs_list.is_err()); } #[test] /// Test adding/removing/editing observations in a dataset (both valid and invalid cases). fn test_manipulate_observations() { + let name = "dataset"; let obs1 = Observation::try_from_str("*1", "o").unwrap(); let obs2 = Observation::try_from_str("00", "p").unwrap(); let obs3 = Observation::try_from_str("11", "q").unwrap(); let initial_obs_list = vec![obs1.clone(), obs2.clone()]; - let mut dataset = Dataset::new(initial_obs_list, vec!["a", "b"]).unwrap(); + let mut dataset = Dataset::new(name, initial_obs_list, vec!["a", "b"]).unwrap(); // add observation dataset.push_obs(obs3.clone()).unwrap(); @@ -457,9 +490,10 @@ mod tests { #[test] /// Test changing observation's ID (both valid and invalid cases). fn test_set_observation_id() { + let name = "dataset"; let obs1 = Observation::try_from_str("*1", "o").unwrap(); let obs2 = Observation::try_from_str("00", "p").unwrap(); - let mut dataset = Dataset::new(vec![obs1, obs2], vec!["a", "b"]).unwrap(); + let mut dataset = Dataset::new(name, vec![obs1, obs2], vec!["a", "b"]).unwrap(); // valid case dataset.set_obs_id_by_str("o", "o2").unwrap(); @@ -472,7 +506,8 @@ mod tests { #[test] /// Test changing variable's ID (both valid and invalid cases). fn test_set_var_id() { - let mut dataset = Dataset::new_empty(vec!["a", "b"]).unwrap(); + let name = "dataset"; + let mut dataset = Dataset::new_empty(name, vec!["a", "b"]).unwrap(); // valid case dataset.set_var_id_by_str("a", "a2").unwrap(); @@ -485,15 +520,16 @@ mod tests { #[test] /// Test removing variable from a dataset. fn test_remove_variable() { + let name = "dataset"; let obs1 = Observation::try_from_str("*1", "o").unwrap(); let obs2 = Observation::try_from_str("00", "p").unwrap(); - let mut dataset = Dataset::new(vec![obs1, obs2], vec!["a", "b"]).unwrap(); + let mut dataset = Dataset::new(name, vec![obs1, obs2], vec!["a", "b"]).unwrap(); dataset.remove_var_by_str("a").unwrap(); let obs1_expected = Observation::try_from_str("1", "o").unwrap(); let obs2_expected = Observation::try_from_str("0", "p").unwrap(); let obs_expected = vec![obs1_expected, obs2_expected]; - let dataset_expected = Dataset::new(obs_expected, vec!["b"]).unwrap(); + let dataset_expected = Dataset::new(name, obs_expected, vec!["b"]).unwrap(); assert_eq!(dataset, dataset_expected); } @@ -501,15 +537,16 @@ mod tests { #[test] /// Test adding variable with default values to a dataset. fn test_add_variable_default() { + let name = "dataset"; let obs1 = Observation::try_from_str("1", "o").unwrap(); let obs2 = Observation::try_from_str("0", "p").unwrap(); - let mut dataset = Dataset::new(vec![obs1, obs2], vec!["b"]).unwrap(); + let mut dataset = Dataset::new(name, vec![obs1, obs2], vec!["b"]).unwrap(); dataset.add_var_default_by_str("a", 0).unwrap(); let obs1_expected = Observation::try_from_str("*1", "o").unwrap(); let obs2_expected = Observation::try_from_str("*0", "p").unwrap(); let obs_expected = vec![obs1_expected, obs2_expected]; - let dataset_expected = Dataset::new(obs_expected, vec!["a", "b"]).unwrap(); + let dataset_expected = Dataset::new(name, obs_expected, vec!["a", "b"]).unwrap(); assert_eq!(dataset, dataset_expected); } @@ -517,9 +554,10 @@ mod tests { #[test] /// Test displaying of string description of datasets. fn test_debug_str() { + let name = "dataset"; let obs1 = Observation::try_from_str("*1", "o").unwrap(); let obs2 = Observation::try_from_str("00", "p").unwrap(); - let dataset = Dataset::new(vec![obs1, obs2], vec!["a", "b"]).unwrap(); + let dataset = Dataset::new(name, vec![obs1, obs2], vec!["a", "b"]).unwrap(); let full_str = "2 observations with vars [a, b]: [o(*1), p(00)]"; let short_str = "2 observations with vars [a, b]"; diff --git a/src-tauri/src/sketchbook/observations/_dataset/_impl_events.rs b/src-tauri/src/sketchbook/observations/_dataset/_impl_events.rs index 6bd38fe..9c2e903 100644 --- a/src-tauri/src/sketchbook/observations/_dataset/_impl_events.rs +++ b/src-tauri/src/sketchbook/observations/_dataset/_impl_events.rs @@ -133,7 +133,7 @@ impl Dataset { let new_obs_data = ObservationData::from_json_str(&payload)?; let new_obs = new_obs_data.to_observation()?; let orig_obs = self.get_obs(&obs_id)?; - if orig_obs == &new_obs { + if orig_obs.get_values() == new_obs.get_values() { return Ok(Consumed::NoChange); } @@ -142,11 +142,30 @@ impl Dataset { self.swap_obs_data(&obs_id, new_obs.get_values().clone())?; let state_change = mk_obs_state_change(&["set_obs_content"], &new_obs_data); - // prepare the reverse event (setting the original ID back) + // prepare the reverse event (setting the original content back) let reverse_at_path = [dataset_id.as_str(), obs_id.as_str(), "set_content"]; let payload = orig_obs_data.to_json_str(); let reverse_event = mk_obs_event(&reverse_at_path, Some(&payload)); Ok(make_reversible(state_change, event, reverse_event)) + } else if action == "set_name" { + // get the payload - string encoding a new name + let new_name = Self::clone_payload_str(event, component_name)?; + let orig_obs = self.get_obs(&obs_id)?; + let orig_name = orig_obs.get_name().to_string(); + if orig_name == new_name { + return Ok(Consumed::NoChange); + } + + // perform the action, prepare the state-change variant (move id from path to payload) + self.set_obs_name(&obs_id, &new_name)?; + let new_obs = self.get_obs(&obs_id)?; + let new_obs_data = ObservationData::from_obs(new_obs, &dataset_id); + let state_change = mk_obs_state_change(&["set_obs_name"], &new_obs_data); + + // prepare the reverse event (setting the original name back) + let reverse_at_path = [dataset_id.as_str(), obs_id.as_str(), "set_name"]; + let reverse_event = mk_obs_event(&reverse_at_path, Some(&orig_name)); + Ok(make_reversible(state_change, event, reverse_event)) } else { AeonError::throw(format!( "`{component_name}` cannot perform action `{action}`." diff --git a/src-tauri/src/sketchbook/observations/_dataset/_impl_serde.rs b/src-tauri/src/sketchbook/observations/_dataset/_impl_serde.rs index 1b82113..6cbb398 100644 --- a/src-tauri/src/sketchbook/observations/_dataset/_impl_serde.rs +++ b/src-tauri/src/sketchbook/observations/_dataset/_impl_serde.rs @@ -16,6 +16,7 @@ impl Serialize for Dataset { S: Serializer, { let mut state = serializer.serialize_struct("Dataset", 4)?; + state.serialize_field("name", &self.name)?; state.serialize_field("observations", &self.observations)?; state.serialize_field("variables", &self.variables)?; @@ -35,6 +36,7 @@ impl<'de> Deserialize<'de> for Dataset { D: Deserializer<'de>, { enum Field { + Name, Observations, Variables, IndexMap, @@ -51,7 +53,7 @@ impl<'de> Deserialize<'de> for Dataset { type Value = Field; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - formatter.write_str("`observations`, `variables`, or `index_map`") + formatter.write_str("`name`, `observations`, `variables`, or `index_map`") } fn visit_str(self, value: &str) -> Result @@ -59,6 +61,7 @@ impl<'de> Deserialize<'de> for Dataset { E: de::Error, { match value { + "name" => Ok(Field::Name), "observations" => Ok(Field::Observations), "variables" => Ok(Field::Variables), "index_map" => Ok(Field::IndexMap), @@ -83,6 +86,7 @@ impl<'de> Deserialize<'de> for Dataset { where V: MapAccess<'de>, { + let mut name = None; let mut observations = None; let mut variables = None; let mut index_map = None; @@ -95,6 +99,12 @@ impl<'de> Deserialize<'de> for Dataset { } observations = Some(map.next_value()?); } + Field::Name => { + if name.is_some() { + return Err(de::Error::duplicate_field("name")); + } + name = Some(map.next_value()?); + } Field::Variables => { if variables.is_some() { return Err(de::Error::duplicate_field("variables")); @@ -111,11 +121,13 @@ impl<'de> Deserialize<'de> for Dataset { } } + let name = name.ok_or_else(|| de::Error::missing_field("name"))?; let observations = observations.ok_or_else(|| de::Error::missing_field("observations"))?; let variables = variables.ok_or_else(|| de::Error::missing_field("variables"))?; let index_map = index_map.ok_or_else(|| de::Error::missing_field("index_map"))?; Ok(Dataset { + name, observations, variables, index_map, @@ -123,7 +135,7 @@ impl<'de> Deserialize<'de> for Dataset { } } - const FIELDS: &[&str] = &["observations", "variables", "index_map"]; + const FIELDS: &[&str] = &["name", "observations", "variables", "index_map"]; deserializer.deserialize_struct("Dataset", FIELDS, DatasetVisitor) } } diff --git a/src-tauri/src/sketchbook/observations/_dataset/mod.rs b/src-tauri/src/sketchbook/observations/_dataset/mod.rs index 821fbcd..8d667ba 100644 --- a/src-tauri/src/sketchbook/observations/_dataset/mod.rs +++ b/src-tauri/src/sketchbook/observations/_dataset/mod.rs @@ -20,6 +20,7 @@ mod _impl_serde; /// of the `ObservationManager`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Dataset { + name: String, /// List of binarized observations. observations: Vec, /// Variables captured by the observations. @@ -33,10 +34,3 @@ impl<'de> JsonSerde<'de> for Dataset {} // We give `Manager` trait to Dataset as it simplifies many things. // It really behaves like a manager class, but is slightly different than the other ones. impl Manager for Dataset {} - -impl Default for Dataset { - /// Default dataset instance with no Variables or Observations. - fn default() -> Dataset { - Dataset::new_empty(Vec::new()).unwrap() - } -} diff --git a/src-tauri/src/sketchbook/observations/_manager/_impl_load_dataset.rs b/src-tauri/src/sketchbook/observations/_manager/_impl_load_dataset.rs index 7de62c7..edded1a 100644 --- a/src-tauri/src/sketchbook/observations/_manager/_impl_load_dataset.rs +++ b/src-tauri/src/sketchbook/observations/_manager/_impl_load_dataset.rs @@ -11,7 +11,7 @@ impl ObservationManager { /// Observation1,0,1,0,1,0,1 /// Observation2,1,0,*,1,0,* /// - pub fn load_dataset(csv_path: &str) -> Result { + pub fn load_dataset(name: &str, csv_path: &str) -> Result { let csv_file = File::open(csv_path).map_err(|e| e.to_string())?; let mut rdr = csv::Reader::from_reader(csv_file); @@ -26,7 +26,7 @@ impl ObservationManager { if record.is_empty() { return Err("Cannot import empty observation.".to_string()); } - let id = record.get(0).unwrap(); + let id: &str = record.get(0).unwrap(); let values: Vec = record .iter() .skip(1) @@ -35,13 +35,14 @@ impl ObservationManager { let observation = Observation::new(values, id)?; observations.push(observation); } - Dataset::new(observations, variables) + Dataset::new(name, observations, variables) } /// Load a dataset from given CSV file, and add it to this `ObservationManager`. The header /// line specifies variables, following lines represent individual observations (id and values). pub fn load_and_add_dataset(&mut self, csv_path: &str, id: &str) -> Result<(), String> { - let dataset = Self::load_dataset(csv_path)?; + // use same name as ID + let dataset = Self::load_dataset(id, csv_path)?; self.add_dataset_by_str(id, dataset) } } diff --git a/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs b/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs index 00a5265..ff3cfcc 100644 --- a/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs +++ b/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs @@ -90,6 +90,20 @@ impl ObservationManager { self.swap_dataset_content(&dataset_id, new_content) } + /// Set name of a dataset with given id. The name must be valid name string. + pub fn set_dataset_name(&mut self, id: &DatasetId, name: &str) -> Result<(), String> { + self.assert_valid_dataset(id)?; + let dataset = self.datasets.get_mut(id).unwrap(); + dataset.set_name(name)?; + Ok(()) + } + + /// Set name of a dataset with given string id. The name must be valid name string. + pub fn set_dataset_name_by_str(&mut self, id: &str, name: &str) -> Result<(), String> { + let dataset_id = DatasetId::new(id)?; + self.set_dataset_name(&dataset_id, name) + } + /// Set the id of dataset with `original_id` to `new_id`. pub fn set_dataset_id( &mut self, @@ -159,6 +173,23 @@ impl ObservationManager { self.remove_var(&dataset_id, &var_id) } + /// Add variable column and fill all its values (in each existing observation) with *. + pub fn add_var(&mut self, dataset_id: &DatasetId, var_id: VarId) -> Result<(), String> { + self.assert_valid_dataset(dataset_id)?; + let new_var_idx = self.get_dataset(dataset_id)?.num_variables(); + self.datasets + .get_mut(dataset_id) + .unwrap() + .add_var_default(var_id, new_var_idx) + } + + /// Add variable column and fill all its values (in each existing observation) with *. + pub fn add_var_by_str(&mut self, dataset_id: &str, id: &str) -> Result<(), String> { + let dataset_id = DatasetId::new(dataset_id)?; + let var_id = VarId::new(id)?; + self.add_var(&dataset_id, var_id) + } + /// Remove the dataset with given `id` from this manager. /// Returns `Err` in case the `id` is not a valid dataset's identifier. pub fn remove_dataset(&mut self, id: &DatasetId) -> Result<(), String> { @@ -284,8 +315,8 @@ mod tests { let manager = ObservationManager::new_empty(); assert_eq!(manager.num_datasets(), 0); - let d1 = Dataset::new(vec![], vec!["a", "b"]).unwrap(); - let d2 = Dataset::new(vec![], vec!["a", "c"]).unwrap(); + let d1 = Dataset::new("d1", vec![], vec!["a", "b"]).unwrap(); + let d2 = Dataset::new("d2", vec![], vec!["a", "c"]).unwrap(); let dataset_list = vec![("d1", d1.clone()), ("d2", d2.clone())]; let manager = ObservationManager::from_datasets(dataset_list).unwrap(); assert_eq!(manager.num_datasets(), 2); @@ -301,20 +332,20 @@ mod tests { let o1 = Observation::try_from_str("*", "o").unwrap(); let o2 = Observation::try_from_str("0", "p").unwrap(); - let d1 = Dataset::new(vec![o1, o2], vec!["a"]).unwrap(); - let d2 = Dataset::new(vec![], vec!["a", "c"]).unwrap(); + let d1 = Dataset::new("d1", vec![o1, o2], vec!["a"]).unwrap(); + let d2 = Dataset::new("d2", vec![], vec!["a", "c"]).unwrap(); let dataset_list = vec![("d1", d1.clone()), ("d2", d2.clone())]; let mut manager = ObservationManager::from_datasets(dataset_list).unwrap(); assert_eq!(manager.num_datasets(), 2); // add dataset - let d3 = Dataset::new(vec![], vec!["a", "c"]).unwrap(); + let d3 = Dataset::new("d3", vec![], vec!["a", "c"]).unwrap(); manager.add_dataset_by_str("d3", d3.clone()).unwrap(); assert_eq!(manager.num_datasets(), 3); // try adding dataset with the same ID again (should fail) - let d3 = Dataset::new(vec![], vec!["a", "c"]).unwrap(); + let d3 = Dataset::new("d3", vec![], vec!["a", "c"]).unwrap(); assert!(manager.add_multiple_datasets(vec![("d3", d3)]).is_err()); assert_eq!(manager.num_datasets(), 3); @@ -332,7 +363,7 @@ mod tests { fn test_edit_dataset() { let o1 = Observation::try_from_str("*1", "o").unwrap(); let o2 = Observation::try_from_str("00", "p").unwrap(); - let d1 = Dataset::new(vec![o1, o2], vec!["a", "b"]).unwrap(); + let d1 = Dataset::new("d1", vec![o1, o2], vec!["a", "b"]).unwrap(); let dataset_list = vec![("dataset1", d1.clone())]; let mut manager = ObservationManager::from_datasets(dataset_list).unwrap(); @@ -342,7 +373,7 @@ mod tests { assert!(manager.get_dataset_id("d1").is_ok()); // try setting content - let new_dataset = Dataset::new(vec![], vec!["a", "b"]).unwrap(); + let new_dataset = Dataset::new("d1", vec![], vec!["a", "b"]).unwrap(); manager .swap_dataset_content_by_str("d1", new_dataset.clone()) .unwrap(); diff --git a/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs b/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs index 576b36f..5080d96 100644 --- a/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs +++ b/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs @@ -114,9 +114,9 @@ impl ObservationManager { let component_name = "observations"; Self::assert_payload_empty(event, component_name)?; - let dataset = Dataset::default(); - // start indexing at 1 + // generate new ID (and name at the same time), start indexing at 1 let dataset_id = self.generate_dataset_id("dataset", Some(1)); + let dataset = Dataset::default(dataset_id.as_str()); let dataset_data = DatasetData::from_dataset(&dataset_id, &dataset); self.add_dataset(dataset_id, dataset)?; @@ -134,10 +134,10 @@ impl ObservationManager { // get the payload - a path to a csv file with dataset let file_path = Self::clone_payload_str(event, component_name)?; - // load the dataset, generate new ID, and add it - let dataset = Self::load_dataset(&file_path)?; - // start indexing at 1 + // generate new ID (and name at the same time), start indexing at 1 let dataset_id = self.generate_dataset_id("dataset", Some(1)); + // load the dataset and add it + let dataset = Self::load_dataset(dataset_id.as_str(), &file_path)?; let dataset_data = DatasetData::from_dataset(&dataset_id, &dataset); self.add_dataset_by_str(&dataset_data.id, dataset)?; @@ -214,6 +214,26 @@ impl ObservationManager { let payload = orig_dataset_data.to_json_str(); let reverse_event = mk_obs_event(&reverse_at_path, Some(&payload)); Ok(make_reversible(state_change, event, reverse_event)) + } else if Self::starts_with("set_name", at_path).is_some() { + // get the payload - json string encoding a new name + let new_name = Self::clone_payload_str(event, component_name)?; + let orig_dataset = self.get_dataset(&dataset_id)?; + if orig_dataset.get_name() == new_name { + return Ok(Consumed::NoChange); + } + + // perform the event, prepare the state-change variant (move id from path to payload) + let orig_dataset_data = DatasetMetaData::from_dataset(&dataset_id, orig_dataset); + self.set_dataset_name(&dataset_id, &new_name)?; + let new_dataset_data = + DatasetMetaData::from_dataset(&dataset_id, self.get_dataset(&dataset_id)?); + let state_change = mk_obs_state_change(&["set_name"], &new_dataset_data); + + // prepare the reverse event (setting the original ID back) + let reverse_at_path = [dataset_id.as_str(), "set_name"]; + let payload = orig_dataset_data.to_json_str(); + let reverse_event = mk_obs_event(&reverse_at_path, Some(&payload)); + Ok(make_reversible(state_change, event, reverse_event)) } else if Self::starts_with("remove_var", at_path).is_some() { // get the payload - string encoding a new dataset data let var_id_str = Self::clone_payload_str(event, component_name)?; @@ -224,6 +244,23 @@ impl ObservationManager { let new_dataset_data = DatasetData::from_dataset(&dataset_id, new_dataset); let state_change = mk_obs_state_change(&["remove_var"], &new_dataset_data); + // TODO: make this potentially reversible? + Ok(Consumed::Irreversible { + state_change, + reset: true, + }) + } else if Self::starts_with("add_var", at_path).is_some() { + Self::assert_payload_empty(event, component_name)?; + + // prepare the placeholder var and add it + let var_id = self.generate_var_id(&dataset_id, "var", Some(1)); + self.add_var(&dataset_id, var_id)?; + + // prepare the state-change variant (move id from path to payload) + let new_dataset = self.get_dataset(&dataset_id)?; + let new_dataset_data = DatasetData::from_dataset(&dataset_id, new_dataset); + let state_change = mk_obs_state_change(&["add_var"], &new_dataset_data); + // TODO: make this potentially reversible? Ok(Consumed::Irreversible { state_change, diff --git a/src-tauri/src/sketchbook/observations/_observation.rs b/src-tauri/src/sketchbook/observations/_observation.rs index 90a4b2f..d15fa72 100644 --- a/src-tauri/src/sketchbook/observations/_observation.rs +++ b/src-tauri/src/sketchbook/observations/_observation.rs @@ -1,5 +1,5 @@ -use crate::sketchbook::ids::ObservationId; use crate::sketchbook::observations::_var_value::VarValue; +use crate::sketchbook::{ids::ObservationId, utils::assert_name_valid}; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -7,31 +7,45 @@ use std::str::FromStr; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Observation { id: ObservationId, + name: String, values: Vec, } /// Creating observations. impl Observation { /// Create `Observation` object from a vector with values, and string ID (which must be - /// a valid identifier). + /// a valid identifier). Name is initialized same as ID. pub fn new(values: Vec, id: &str) -> Result { Ok(Self { values, id: ObservationId::new(id)?, + name: id.to_string(), }) } - /// Create `Observation` encoding a vector of `n` ones. + /// Create `Observation` object from a vector with values, string ID (which must be + /// a valid identifier), and name. + pub fn new_with_name(values: Vec, id: &str, name: &str) -> Result { + assert_name_valid(name)?; + Ok(Self { + values, + id: ObservationId::new(id)?, + name: name.to_string(), + }) + } + + /// Create `Observation` encoding a vector of `n` ones. Name is initialized same as ID. pub fn new_full_ones(n: usize, id: &str) -> Result { Self::new(vec![VarValue::True; n], id) } - /// Create `Observation` encoding a vector of `n` zeros. + /// Create `Observation` encoding a vector of `n` zeros. Name is initialized same as ID. pub fn new_full_zeros(n: usize, id: &str) -> Result { Self::new(vec![VarValue::False; n], id) } /// Create `Observation` encoding a vector of `n` unspecified values. + /// Name is initialized same as ID. pub fn new_full_unspecified(n: usize, id: &str) -> Result { Self::new(vec![VarValue::Any; n], id) } @@ -39,7 +53,7 @@ impl Observation { /// Create `Observation` object from string encoding of its (ordered) values. /// Values are encoded using characters `1`, `0`, or `*`. /// - /// Observation cannot be empty. + /// Observation cannot be empty. Name is initialized same as ID. pub fn try_from_str(observation_str: &str, id: &str) -> Result { let mut observation_vec: Vec = Vec::new(); for c in observation_str.chars() { @@ -51,10 +65,27 @@ impl Observation { Self::new(observation_vec, id) } + + /// Create `Observation` object from string encoding of its (ordered) values. + /// Values are encoded using characters `1`, `0`, or `*`. + /// + /// Observation cannot be empty. + pub fn try_from_str_named(observation_str: &str, id: &str, name: &str) -> Result { + let mut obs = Self::try_from_str(observation_str, id)?; + obs.set_name(name)?; + Ok(obs) + } } /// Editing observations. impl Observation { + /// Set name. + pub fn set_name(&mut self, name: &str) -> Result<(), String> { + assert_name_valid(name)?; + self.name = name.to_string(); + Ok(()) + } + /// Set the value at given idx. pub fn set_value(&mut self, index: usize, value: VarValue) -> Result<(), String> { if index >= self.num_values() { @@ -124,6 +155,11 @@ impl Observation { /// Observing `Observation` instances. impl Observation { + /// Get observation's name. + pub fn get_name(&self) -> &str { + &self.name + } + /// Get reference to observation's vector of values. pub fn get_values(&self) -> &Vec { &self.values diff --git a/src/aeon_events.ts b/src/aeon_events.ts index 75700ef..9a9b965 100644 --- a/src/aeon_events.ts +++ b/src/aeon_events.ts @@ -251,6 +251,10 @@ interface AeonState { datasetIdChanged: Observable /** Set ID of dataset with given original ID to a new id. */ setDatasetId: (originalId: string, newId: string) => void + /** DatasetMetaData (with updated `name`) of a modified dataset. */ + datasetNameChanged: Observable + /** Set name of dataset with given ID. */ + setDatasetName: (id: string, newName: string) => void /** DatasetData of a fully modified dataset. */ datasetContentChanged: Observable /** Set content (variables, observations - everything) of dataset with given ID. */ @@ -260,9 +264,13 @@ interface AeonState { /** Set variable's ID within a specified dataset. */ setDatasetVariable: (datasetId: string, originalId: string, newId: string) => void /** DatasetData (with updated both `variables` and `observations`) of a modified dataset. */ - datasetVariableRemoved: Observable + datasetVariableRemoved: Observable /** Remove variable from a specified dataset (removing "a column" of a dataset's table). */ removeDatasetVariable: (datasetId: string, varId: string) => void + /** DatasetData (with updated both `variables` and `observations`) of a modified dataset. */ + datasetVariableAdded: Observable + /** Add (placeholder) variable to a specified dataset (adding an empty column to a dataset's table). */ + addDatasetVariable: (datasetId: string) => void /** ObservationData for a newly pushed observation (also contains corresponding dataset ID). */ observationPushed: Observable @@ -285,6 +293,10 @@ interface AeonState { observationContentChanged: Observable /** Modify a content of a particular observation. */ setObservationContent: (datasetId: string, observation: ObservationData) => void + /** ObservationData for an observation with modified name (also contains corresponding dataset ID). */ + observationNameChanged: Observable + /** Modify a name of a particular observation. */ + setObservationName: (datasetId: string, observation: ObservationData) => void } /** The state of the dynamic and static properties. */ @@ -461,6 +473,7 @@ export interface LayoutNodeDataPrototype { /** An object representing basic information regarding an observation (in a particular dataset). */ export interface ObservationData { id: string + name: string dataset: string values: string // string with `0`/`1`/`*`, for instance: "0001**110" } @@ -468,6 +481,7 @@ export interface ObservationData { /** An object representing all information regarding a whole dataset. */ export interface DatasetData { id: string + name: string observations: ObservationData[] variables: string[] } @@ -478,6 +492,7 @@ export interface DatasetData { * */ export interface DatasetMetaData { id: string + name: string variables: [string] } @@ -1200,14 +1215,17 @@ export const aeonState: AeonState = { datasetRemoved: new Observable(['sketch', 'observations', 'remove']), datasetIdChanged: new Observable(['sketch', 'observations', 'set_id']), datasetContentChanged: new Observable(['sketch', 'observations', 'set_content']), + datasetNameChanged: new Observable(['sketch', 'observations', 'set_name']), datasetVariableChanged: new Observable(['sketch', 'observations', 'set_var_id']), - datasetVariableRemoved: new Observable(['sketch', 'observations', 'remove_var']), + datasetVariableRemoved: new Observable(['sketch', 'observations', 'remove_var']), + datasetVariableAdded: new Observable(['sketch', 'observations', 'add_var']), observationPushed: new Observable(['sketch', 'observations', 'push_obs']), observationPopped: new Observable(['sketch', 'observations', 'pop_obs']), observationRemoved: new Observable(['sketch', 'observations', 'remove_obs']), observationIdChanged: new Observable(['sketch', 'observations', 'set_obs_id']), observationContentChanged: new Observable(['sketch', 'observations', 'set_obs_content']), + observationNameChanged: new Observable(['sketch', 'observations', 'set_obs_name']), addDataset (id: string, variables: string[], observations: ObservationData[]): void { aeonEvents.emitAction({ @@ -1249,6 +1267,12 @@ export const aeonState: AeonState = { payload: JSON.stringify(newContent) }) }, + setDatasetName (id: string, newName: string): void { + aeonEvents.emitAction({ + path: ['sketch', 'observations', id, 'set_name'], + payload: newName + }) + }, setDatasetVariable (datasetId: string, originalId: string, newId: string): void { aeonEvents.emitAction({ path: ['sketch', 'observations', datasetId, 'set_variable'], @@ -1261,6 +1285,12 @@ export const aeonState: AeonState = { payload: varId }) }, + addDatasetVariable (datasetId: string): void { + aeonEvents.emitAction({ + path: ['sketch', 'observations', datasetId, 'add_var'], + payload: null + }) + }, pushObservation (datasetId: string, observation?: ObservationData): void { if (observation === undefined) { aeonEvents.emitAction({ @@ -1297,6 +1327,12 @@ export const aeonState: AeonState = { path: ['sketch', 'observations', datasetId, observation.id, 'set_content'], payload: JSON.stringify(observation) }) + }, + setObservationName (datasetId: string, observation: ObservationData): void { + aeonEvents.emitAction({ + path: ['sketch', 'observations', datasetId, observation.id, 'set_name'], + payload: observation.name + }) } }, properties: { diff --git a/src/html/component-editor/observations-editor/observations-editor.less b/src/html/component-editor/observations-editor/observations-editor.less index 745fd21..efbaee1 100644 --- a/src/html/component-editor/observations-editor/observations-editor.less +++ b/src/html/component-editor/observations-editor/observations-editor.less @@ -6,7 +6,7 @@ .uk-flex-between; .uk-flex-middle; .uk-box-shadow-small; - padding: 0.5em 1em; + padding: 0.5em 2em 0.5em 1em; position: sticky; top: 0; z-index: 99; @@ -17,13 +17,28 @@ color: @text-dark; } -.set-name { +.set-name, .set-data-id { &:read-only { background-color: transparent; border: none; } } +.name-id-container { + display: flex; + align-items: center; + gap: 0.5em; +} + +.set-name, +.set-data-id { + flex: 0.2; + padding: 0; +} +.set-name { + margin: 0.5em; +} + .accordion { .container { @@ -90,9 +105,13 @@ overflow-x: hidden; } -.import-button { +.buttons-container { + display: flex; + gap: 1.5em; +} + +.import-button, .create-button { height: 2em; - right: -1em; } .heading { @@ -104,12 +123,11 @@ color: white; } - .set-name { + .set-name, .set-data-id { &:read-only { color: white; } } - } @media (prefers-color-scheme: light) { diff --git a/src/html/component-editor/observations-editor/observations-editor.ts b/src/html/component-editor/observations-editor/observations-editor.ts index 0bde965..716cad0 100644 --- a/src/html/component-editor/observations-editor/observations-editor.ts +++ b/src/html/component-editor/observations-editor/observations-editor.ts @@ -11,6 +11,7 @@ import { debounce } from 'lodash' import { functionDebounceTimer } from '../../util/config' import { aeonState, + type DatasetMetaData, type DatasetData, type DatasetIdUpdateData, type ObservationData, @@ -28,22 +29,33 @@ export default class ObservationsEditor extends LitElement { constructor () { super() + // changes to whole datasets triggered by table buttons + this.addEventListener('change-dataset-id', this.changeDatasetId) this.addEventListener('rename-dataset', this.renameDataset) + this.addEventListener('push-new-observation', this.pushNewObservation) + this.addEventListener('remove-observation', this.removeObservation) + this.addEventListener('remove-dataset', (e) => { void this.removeDataset(e) }) + this.addEventListener('add-dataset-variable', this.addVariable) - // observations-related event listeners + // changes to observations triggered by table edits + this.addEventListener('change-observation', this.changeObservation) + + // event listeners for backend updates aeonState.sketch.observations.datasetLoaded.addEventListener(this.#onDatasetLoaded.bind(this)) + aeonState.sketch.observations.datasetCreated.addEventListener(this.#onDatasetCreated.bind(this)) + aeonState.sketch.observations.datasetRemoved.addEventListener(this.#onDatasetRemoved.bind(this)) + aeonState.sketch.observations.datasetContentChanged.addEventListener(this.#onDatasetContentChanged.bind(this)) aeonState.sketch.observations.datasetIdChanged.addEventListener(this.#onDatasetIdChanged.bind(this)) - this.addEventListener('push-new-observation', this.pushNewObservation) + aeonState.sketch.observations.datasetNameChanged.addEventListener(this.#onDatasetNameChanged.bind(this)) + aeonState.sketch.observations.observationPushed.addEventListener(this.#onObservationPushed.bind(this)) - this.addEventListener('remove-observation', this.removeObservation) aeonState.sketch.observations.observationRemoved.addEventListener(this.#onObservationRemoved.bind(this)) - this.addEventListener('change-observation', this.changeObservation) - aeonState.sketch.observations.observationContentChanged.addEventListener(this.#onObservationContentChanged.bind(this)) + aeonState.sketch.observations.observationIdChanged.addEventListener(this.#onObservationIdChanged.bind(this)) - this.addEventListener('remove-dataset', (e) => { void this.removeDataset(e) }) - aeonState.sketch.observations.datasetRemoved.addEventListener(this.#onDatasetRemoved.bind(this)) - // TODO add all other events + // these two handled the same way + aeonState.sketch.observations.observationContentChanged.addEventListener(this.#onObservationContentChanged.bind(this)) + aeonState.sketch.observations.observationNameChanged.addEventListener(this.#onObservationContentChanged.bind(this)) // refresh-event listeners aeonState.sketch.observations.datasetsRefreshed.addEventListener(this.#onDatasetsRefreshed.bind(this)) @@ -57,7 +69,7 @@ export default class ObservationsEditor extends LitElement { } private convertToIObservation (observationData: ObservationData, variables: string[]): IObservation { - const obs: IObservation = { id: observationData.id, name: observationData.id, selected: false } + const obs: IObservation = { id: observationData.id, name: observationData.name, selected: false } variables.forEach(((v, idx) => { const value = observationData.values[idx] obs[v] = (value === '*') ? '' : value @@ -69,7 +81,7 @@ export default class ObservationsEditor extends LitElement { const valueString = variables.map(v => { return (observation[v] === '') ? '*' : observation[v] }).join('') - return { id: observation.id, dataset: datasetId, values: valueString } + return { id: observation.id, name: observation.name, dataset: datasetId, values: valueString } } private convertToIObservationSet (datasetData: DatasetData): IObservationSet { @@ -78,6 +90,7 @@ export default class ObservationsEditor extends LitElement { ) return { id: datasetData.id, + name: datasetData.name, observations, variables: datasetData.variables } @@ -89,6 +102,7 @@ export default class ObservationsEditor extends LitElement { ) return { id: dataset.id, + name: dataset.name, observations, variables: dataset.variables } @@ -148,15 +162,20 @@ export default class ObservationsEditor extends LitElement { x: pos.x + (size.width / 2) - 200, y: pos.y + size.height / 4 }) + + // Once loaded, show the dialog to edit and import the dataset. void importDialog.once('loaded', () => { void importDialog.emit('observations_import_update', { data, variables }) }) + + // Handle the case when data are successfully edited and imported void importDialog.once('observations_import_dialog', (event: TauriEvent) => { const modifiedDataset: IObservationSet = { id: name, + name, observations: event.payload, variables } @@ -164,6 +183,13 @@ export default class ObservationsEditor extends LitElement { this.updateObservations(this.contentData.observations.concat(modifiedDataset)) aeonState.sketch.observations.setDatasetContent(name, this.convertFromIObservationSet(modifiedDataset)) }) + + // Handle the case when the dialog is closed/cancelled + void importDialog.once('observations_import_cancelled', () => { + console.log('Import dialog was closed or cancelled.') + // the dataset was temporarily added in its original form, now we just remove it + aeonState.sketch.observations.removeDataset(name) + }) } #onDatasetContentChanged (data: DatasetData): void { @@ -176,12 +202,28 @@ export default class ObservationsEditor extends LitElement { this.updateObservations(datasets) } + private createDataset (): void { + aeonState.sketch.observations.addDefaultDataset() + } + + #onDatasetCreated (data: DatasetData): void { + console.log('Adding new dataset.') + const newDataset = this.convertToIObservationSet(data) + this.updateObservations(this.contentData.observations.concat(newDataset)) + } + updateDatasetId = debounce((newId: string, index: number) => { const originalId = this.contentData.observations[index].id aeonState.sketch.observations.setDatasetId(originalId, newId) }, functionDebounceTimer ) + updateDatasetName = debounce((newName: string, index: number) => { + const originalId = this.contentData.observations[index].id + aeonState.sketch.observations.setDatasetName(originalId, newName) + }, functionDebounceTimer + ) + #onDatasetIdChanged (data: DatasetIdUpdateData): void { console.log(data) const index = this.contentData.observations.findIndex(d => d.id === data.original_id) @@ -194,6 +236,19 @@ export default class ObservationsEditor extends LitElement { this.updateObservations(datasets) } + #onDatasetNameChanged (data: DatasetMetaData): void { + console.log(data) + const datasetIndex = this.contentData.observations.findIndex(d => d.id === data.id) + if (datasetIndex === -1) return + + const datasets = structuredClone(this.contentData.observations) + datasets[datasetIndex] = { + ...datasets[datasetIndex], + name: data.name + } + this.updateObservations(datasets) + } + private pushNewObservation (event: Event): void { // push new observation (placeholder) that is fully generated on backend const detail = (event as CustomEvent).detail @@ -214,6 +269,13 @@ export default class ObservationsEditor extends LitElement { aeonState.sketch.observations.removeObservation(detail.dataset, detail.id) } + private addVariable (event: Event): void { + // add new variable (placeholder) that is fully generated on backend + const detail = (event as CustomEvent).detail + aeonState.sketch.observations.addDatasetVariable(detail.id) + aeonState.sketch.observations.refreshDatasets() + } + #onObservationRemoved (data: ObservationData): void { const datasetIndex = this.contentData.observations.findIndex(d => d.id === data.dataset) if (datasetIndex === -1) return @@ -230,7 +292,10 @@ export default class ObservationsEditor extends LitElement { aeonState.sketch.observations.setObservationId(dataset.id, detail.id, detail.observation.id) } const obsData = this.convertFromIObservation(detail.observation, dataset.id, dataset.variables) - aeonState.sketch.observations.setObservationContent(detail.dataset, obsData) + if (detail.name !== obsData.name) { + aeonState.sketch.observations.setObservationName(dataset.id, obsData) + } + aeonState.sketch.observations.setObservationContent(dataset.id, obsData) } #onObservationContentChanged (data: ObservationData): void { @@ -251,7 +316,6 @@ export default class ObservationsEditor extends LitElement { if (obsIndex === -1) return const datasets: IObservationSet[] = structuredClone(this.contentData.observations) datasets[datasetIndex].observations[obsIndex].id = data.new_id - datasets[datasetIndex].observations[obsIndex].name = data.new_id this.updateObservations(datasets) } @@ -271,6 +335,12 @@ export default class ObservationsEditor extends LitElement { (this.shadowRoot?.querySelector('#set-name-' + this.datasetRenameIndex) as HTMLInputElement)?.focus() } + changeDatasetId (event: Event): void { + const detail = (event as CustomEvent).detail + this.datasetRenameIndex = this.contentData.observations.findIndex(d => d.id === detail.id); + (this.shadowRoot?.querySelector('#set-data-id-' + this.datasetRenameIndex) as HTMLInputElement)?.focus() + } + async removeDataset (event: Event): Promise { if (!await dialog.confirm('Remove dataset?')) return const detail = (event as CustomEvent).detail @@ -298,27 +368,47 @@ export default class ObservationsEditor extends LitElement {

Observations

- +
+ + +
${this.contentData?.observations.length === 0 ? html`
No observations loaded
` : ''}
${map(this.contentData.observations, (dataset, index) => html`
-
+ }}"> + + ID = + +
${when(this.shownDatasets.includes(index), () => html`
{ + void emit('observations_import_cancelled', {}) + void appWindow.close() + }) } async firstUpdated (): Promise { @@ -41,7 +47,8 @@ export default class ObservationsImport extends LitElement { createColumns (): ColumnDefinition[] { const columns: ColumnDefinition[] = [ checkboxColumn, - nameColumn + nameColumn(true), + idColumn(true) ] this.variables.forEach(v => { columns.push(dataCell(v)) @@ -75,7 +82,7 @@ export default class ObservationsImport extends LitElement { return html`${when(this.loaded, () => html`
-

Select rows to be imported

+

Select rows to be imported, edit values

${this.table} diff --git a/src/html/component-editor/observations-editor/observations-set/observations-set.ts b/src/html/component-editor/observations-editor/observations-set/observations-set.ts index 9466bb5..1c932b3 100644 --- a/src/html/component-editor/observations-editor/observations-set/observations-set.ts +++ b/src/html/component-editor/observations-editor/observations-set/observations-set.ts @@ -6,7 +6,7 @@ import { Tabulator, type ColumnDefinition, type CellComponent } from 'tabulator- import { type IObservation, type IObservationSet } from '../../../util/data-interfaces' import { appWindow, WebviewWindow } from '@tauri-apps/api/window' import { type Event as TauriEvent } from '@tauri-apps/api/helpers/event' -import { checkboxColumn, dataCell, loadTabulatorPlugins, nameColumn, tabulatorOptions } from '../tabulator-utility' +import { checkboxColumn, dataCell, loadTabulatorPlugins, idColumn, nameColumn, tabulatorOptions } from '../tabulator-utility' import { icon } from '@fortawesome/fontawesome-svg-core' import { faAdd, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons' @@ -39,7 +39,8 @@ export default class ObservationsSet extends LitElement { private async init (): Promise { const columns: ColumnDefinition[] = [ checkboxColumn, - nameColumn + nameColumn(false), + idColumn(false) ] this.data.variables.forEach(v => { columns.push(dataCell(v)) @@ -103,7 +104,7 @@ export default class ObservationsSet extends LitElement { this.tabulatorReady = true this.tabulator?.on('cellEdited', (cell) => { const data = cell.getData() as IObservation - this.changeObservation(data.id, data) + this.changeObservation(data.id, data.name, data) }) }) } @@ -119,6 +120,16 @@ export default class ObservationsSet extends LitElement { })) } + private addVariable (): void { + this.dispatchEvent(new CustomEvent('add-dataset-variable', { + detail: { + id: this.data.id + }, + bubbles: true, + composed: true + })) + } + private removeObservation (obs: IObservation): void { this.dispatchEvent(new CustomEvent('remove-observation', { detail: { @@ -130,10 +141,11 @@ export default class ObservationsSet extends LitElement { })) } - private changeObservation (id: string, observation: IObservation): void { + private changeObservation (id: string, name: string, observation: IObservation): void { this.dispatchEvent(new CustomEvent('change-observation', { detail: { dataset: this.data.id, + name, id, observation }, @@ -171,7 +183,7 @@ export default class ObservationsSet extends LitElement { this.dialogs[obs.id] = undefined const index = this.data.observations.findIndex(observation => observation.id === obs.id) if (index === -1) return - this.changeObservation(obs.id, event.payload.data) + this.changeObservation(obs.id, obs.name, event.payload.data) }) void renameDialog.onCloseRequested(() => { this.dialogs[obs.id] = undefined @@ -198,6 +210,16 @@ export default class ObservationsSet extends LitElement { })) } + changeDatasetId (): void { + this.dispatchEvent(new CustomEvent('change-dataset-id', { + detail: { + id: this.data.id + }, + bubbles: true, + composed: true + })) + } + render (): TemplateResult { return html`
@@ -207,13 +229,26 @@ export default class ObservationsSet extends LitElement { ${icon(faEdit).node} Rename dataset
- + + +