diff --git a/collab-database/src/database.rs b/collab-database/src/database.rs index 91a55c2d3..e8e3dfcda 100644 --- a/collab-database/src/database.rs +++ b/collab-database/src/database.rs @@ -7,7 +7,8 @@ use crate::blocks::{Block, BlockEvent}; use crate::database_state::DatabaseNotify; use crate::error::DatabaseError; use crate::fields::{ - stringify_type_option, Field, FieldChangeReceiver, FieldMap, FieldUpdate, StringifyTypeOption, + type_option_cell_reader, type_option_cell_writer, Field, FieldChangeReceiver, FieldMap, + FieldUpdate, TypeOptionCellReader, TypeOptionCellWriter, }; use crate::meta::MetaMap; use crate::rows::{ @@ -498,12 +499,26 @@ impl Database { self.body.block.get_row_meta(row_id).await } - pub fn get_stringify_type_option(&self, field_id: &str) -> Option> { + /// Return [TypeOptionCellReader] for the given field id. + pub fn get_cell_reader(&self, field_id: &str) -> Option> { let txn = self.collab.transact(); let field = self.body.fields.get_field(&txn, field_id)?; + drop(txn); + + let field_type = FieldType::from(field.field_type); + let type_option = field.get_any_type_option(field_type.type_id())?; + type_option_cell_reader(type_option, &field_type) + } + + /// Return [TypeOptionCellWriter] for the given field id. + pub fn get_cell_writer(&self, field_id: &str) -> Option> { + let txn = self.collab.transact(); + let field = self.body.fields.get_field(&txn, field_id)?; + drop(txn); + let field_type = FieldType::from(field.field_type); let type_option = field.get_any_type_option(field_type.type_id())?; - stringify_type_option(type_option, &field_type) + type_option_cell_writer(type_option, &field_type) } #[instrument(level = "debug", skip_all)] diff --git a/collab-database/src/fields/type_option/checkbox_type_option.rs b/collab-database/src/fields/type_option/checkbox_type_option.rs index 2c2ec59f1..5fca9e37b 100644 --- a/collab-database/src/fields/type_option/checkbox_type_option.rs +++ b/collab-database/src/fields/type_option/checkbox_type_option.rs @@ -1,5 +1,12 @@ -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::entity::FieldType; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; +use serde_json::Value; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CheckboxTypeOption; @@ -10,12 +17,44 @@ impl CheckboxTypeOption { } } -impl StringifyTypeOption for CheckboxTypeOption { - fn stringify_text(&self, text: &str) -> String { +impl TypeOptionCellReader for CheckboxTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + let value = match cell.get_as::(CELL_DATA) { + None => "".to_string(), + Some(s) => Self::convert_raw_cell_data(self, &s), + }; + Value::String(value) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = cell.get_as::(CELL_DATA)?; + if bool_from_str(&cell_data) { + Some(1.0) + } else { + Some(0.0) + } + } + + fn convert_raw_cell_data(&self, text: &str) -> String { text.to_string() } } +impl TypeOptionCellWriter for CheckboxTypeOption { + fn write_json(&self, value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::Checkbox); + if let Some(data) = match value { + Value::String(s) => Some(s), + Value::Bool(b) => Some(b.to_string()), + Value::Number(n) => Some(n.to_string()), + _ => None, + } { + cell.insert(CELL_DATA.into(), bool_from_str(&data).to_string().into()); + } + cell + } +} + impl From for TypeOptionData { fn from(_data: CheckboxTypeOption) -> Self { TypeOptionDataBuilder::new() @@ -27,3 +66,94 @@ impl From for CheckboxTypeOption { CheckboxTypeOption } } + +fn bool_from_str(s: &str) -> bool { + let lower_case_str: &str = &s.to_lowercase(); + match lower_case_str { + "1" | "true" | "yes" => true, + "0" | "false" | "no" => false, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_checkbox_type_option_json_cell() { + let option = CheckboxTypeOption::new(); + let mut cell = new_cell_builder(FieldType::Checkbox); + cell.insert(CELL_DATA.into(), "true".into()); + + // Convert cell to JSON + let value = option.json_cell(&cell); + assert_eq!(value, Value::String("true".to_string())); + + // Test with empty data + let empty_cell = new_cell_builder(FieldType::Checkbox); + let empty_value = option.json_cell(&empty_cell); + assert_eq!(empty_value, Value::String("".to_string())); + } + + #[test] + fn test_checkbox_type_option_numeric_cell() { + let option = CheckboxTypeOption::new(); + + let mut true_cell = new_cell_builder(FieldType::Checkbox); + true_cell.insert(CELL_DATA.into(), "true".into()); + assert_eq!(option.numeric_cell(&true_cell), Some(1.0)); + + let mut false_cell = new_cell_builder(FieldType::Checkbox); + false_cell.insert(CELL_DATA.into(), "false".into()); + assert_eq!(option.numeric_cell(&false_cell), Some(0.0)); + + let mut invalid_cell = new_cell_builder(FieldType::Checkbox); + invalid_cell.insert(CELL_DATA.into(), "invalid".into()); + assert_eq!(option.numeric_cell(&invalid_cell), Some(0.0)); + } + + #[test] + fn test_checkbox_type_option_write_json() { + let option = CheckboxTypeOption::new(); + + // Write a string + let value = Value::String("true".to_string()); + let cell = option.write_json(value); + assert_eq!(cell.get_as::(CELL_DATA).unwrap(), "true"); + + // Write a boolean + let value = Value::Bool(true); + let cell = option.write_json(value); + assert_eq!(cell.get_as::(CELL_DATA).unwrap(), "true"); + + // Write a number + let value = Value::Number(1.into()); + let cell = option.write_json(value); + assert_eq!(cell.get_as::(CELL_DATA).unwrap(), "true"); + } + + #[test] + fn test_checkbox_type_option_raw_conversion() { + let option = CheckboxTypeOption::new(); + assert_eq!( + option.convert_raw_cell_data("raw data"), + "raw data".to_string() + ); + } + + #[test] + fn test_bool_from_str() { + assert!(bool_from_str("true")); + assert!(bool_from_str("1")); + assert!(bool_from_str("yes")); + + assert!(!bool_from_str("false")); + assert!(!bool_from_str("0")); + assert!(!bool_from_str("no")); + + // Invalid inputs default to false + assert!(!bool_from_str("invalid")); + assert!(!bool_from_str("")); + } +} diff --git a/collab-database/src/fields/type_option/checklist_type_option.rs b/collab-database/src/fields/type_option/checklist_type_option.rs index 285455756..407929455 100644 --- a/collab-database/src/fields/type_option/checklist_type_option.rs +++ b/collab-database/src/fields/type_option/checklist_type_option.rs @@ -1,6 +1,13 @@ -use serde::{Deserialize, Serialize}; - use super::{TypeOptionData, TypeOptionDataBuilder}; +use crate::entity::FieldType; +use crate::fields::select_type_option::SELECTION_IDS_SEPARATOR; +use crate::fields::{TypeOptionCellReader, TypeOptionCellWriter}; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::check_list_parse::ChecklistCellData; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ChecklistTypeOption; @@ -16,3 +23,119 @@ impl From for TypeOptionData { TypeOptionDataBuilder::default() } } + +impl TypeOptionCellReader for ChecklistTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + let cell_data = ChecklistCellData::from(cell); + json!(cell_data) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, cell_data: &str) -> String { + let cell_data = serde_json::from_str::(cell_data).unwrap_or_default(); + cell_data + .options + .into_iter() + .map(|option| option.name) + .collect::>() + .join(SELECTION_IDS_SEPARATOR) + } +} + +impl TypeOptionCellWriter for ChecklistTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap_or_default(); + cell_data.into() + } +} + +impl From<&Cell> for ChecklistCellData { + fn from(cell: &Cell) -> Self { + cell + .get_as::(CELL_DATA) + .map(|data| serde_json::from_str::(&data).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From for Cell { + fn from(cell_data: ChecklistCellData) -> Self { + let data = serde_json::to_string(&cell_data).unwrap_or_default(); + let mut cell = new_cell_builder(FieldType::Checklist); + cell.insert(CELL_DATA.into(), data.into()); + cell + } +} +#[cfg(test)] +mod checklist_type_option_tests { + use super::*; + use serde_json::json; + + #[test] + fn test_json_cell_conversion() { + let checklist_option = ChecklistTypeOption; + + let cell_data = ChecklistCellData::from(( + vec!["Opt1".to_string(), "Opt2".to_string()], + vec!["Opt1".to_string()], + )); + let cell: Cell = cell_data.clone().into(); + + let json_value = checklist_option.json_cell(&cell); + let restored_data: ChecklistCellData = + serde_json::from_value(json_value).expect("Valid JSON value"); + + assert_eq!(restored_data.options.len(), 2); + assert_eq!(restored_data.selected_option_ids.len(), 1); + } + + #[test] + fn test_numeric_cell_conversion() { + let checklist_option = ChecklistTypeOption; + + let cell_data = ChecklistCellData::from(( + vec!["Opt1".to_string(), "Opt2".to_string()], + vec!["Opt1".to_string()], + )); + let cell: Cell = cell_data.clone().into(); + + let numeric_value = checklist_option.numeric_cell(&cell); + assert!(numeric_value.is_none()); + } + + #[test] + fn test_raw_cell_data_conversion() { + let checklist_option = ChecklistTypeOption; + + let cell_data = ChecklistCellData::from(( + vec!["OptA".to_string(), "OptB".to_string()], + vec!["OptA".to_string()], + )); + let cell_data_json = serde_json::to_string(&cell_data).expect("Valid serialization"); + + let converted_data = checklist_option.convert_raw_cell_data(&cell_data_json); + assert_eq!(converted_data, "OptA,OptB"); + } + + #[test] + fn test_write_json_to_cell() { + let checklist_option = ChecklistTypeOption; + + let json_value = json!({ + "options": [ + { "id": "1", "name": "Option1", "color": 0 }, + { "id": "2", "name": "Option2", "color": 1 } + ], + "selected_option_ids": ["1"] + }); + + let cell = checklist_option.write_json(json_value); + let restored_data = ChecklistCellData::from(&cell); + + assert_eq!(restored_data.options.len(), 2); + assert_eq!(restored_data.selected_option_ids.len(), 1); + } +} diff --git a/collab-database/src/fields/type_option/date_type_option.rs b/collab-database/src/fields/type_option/date_type_option.rs index 5e50a279e..a3be14283 100644 --- a/collab-database/src/fields/type_option/date_type_option.rs +++ b/collab-database/src/fields/type_option/date_type_option.rs @@ -1,7 +1,9 @@ use crate::entity::FieldType; use crate::error::DatabaseError; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; use crate::rows::{new_cell_builder, Cell}; use crate::template::entity::CELL_DATA; use chrono::{FixedOffset, Local, MappedLocalTime, NaiveDateTime, NaiveTime, Offset, TimeZone}; @@ -11,6 +13,8 @@ use serde::de::Visitor; use serde::{Deserialize, Serialize}; use std::fmt; +use crate::template::time_parse::TimeCellData; +use serde_json::{json, Value}; use std::str::FromStr; pub use strum::IntoEnumIterator; pub use strum_macros::EnumIter; @@ -19,9 +23,27 @@ use yrs::Any; #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct TimeTypeOption; -impl StringifyTypeOption for TimeTypeOption { - fn stringify_text(&self, text: &str) -> String { - text.to_string() +impl TypeOptionCellReader for TimeTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + let cell_data = TimeCellData::from(cell); + json!(cell_data) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = TimeCellData::from(cell); + cell_data.0.map(|timestamp| timestamp as f64) + } + + fn convert_raw_cell_data(&self, text: &str) -> String { + let cell_data = TimeCellData::from(text); + cell_data.to_string() + } +} + +impl TypeOptionCellWriter for TimeTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap_or_default(); + Cell::from(&cell_data) } } @@ -44,15 +66,19 @@ pub struct DateTypeOption { pub timezone_id: String, } -impl StringifyTypeOption for DateTypeOption { - fn stringify_cell(&self, cell: &Cell) -> String { +impl TypeOptionCellReader for DateTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { let cell_data = DateCellData::from(cell); + json!(cell_data) + } + + fn stringify_cell(&self, cell_data: &Cell) -> String { + let cell_data = DateCellData::from(cell_data); let include_time = cell_data.include_time; let timestamp = cell_data.timestamp; let is_range = cell_data.is_range; let (date, time) = self.formatted_date_time_from_timestamp(×tamp); - if is_range { let (end_date, end_time) = match cell_data.end_timestamp { Some(timestamp) => self.formatted_date_time_from_timestamp(&Some(timestamp)), @@ -74,7 +100,12 @@ impl StringifyTypeOption for DateTypeOption { } } - fn stringify_text(&self, text: &str) -> String { + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = DateCellData::from(cell); + cell_data.timestamp.map(|timestamp| timestamp as f64) + } + + fn convert_raw_cell_data(&self, text: &str) -> String { match text.parse::() { Ok(timestamp) => { let cell = DateCellData::from_timestamp(timestamp); @@ -85,6 +116,13 @@ impl StringifyTypeOption for DateTypeOption { } } +impl TypeOptionCellWriter for DateTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap(); + Cell::from(&cell_data) + } +} + impl DateTypeOption { pub fn new() -> Self { Self { @@ -258,7 +296,7 @@ impl TimeFormat { } } -#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, Default, Eq, PartialEq)] pub enum DateFormat { Local = 0, US = 1, @@ -382,21 +420,18 @@ impl From<&DateCellData> for Cell { cell } } - impl<'de> serde::Deserialize<'de> for DateCellData { fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de>, { - struct DateCellVisitor(); + struct DateCellVisitor; impl<'de> Visitor<'de> for DateCellVisitor { type Value = DateCellData; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "DateCellData with type: str containing either an integer timestamp or the JSON representation", - ) + formatter.write_str("a JSON object representing DateCellData or an integer timestamp") } fn visit_i64(self, value: i64) -> Result @@ -429,47 +464,198 @@ impl<'de> serde::Deserialize<'de> for DateCellData { let mut is_range: Option = None; let mut reminder_id: Option = None; - while let Some(key) = map.next_key()? { - match key { + while let Some(key) = map.next_key::()? { + match key.as_str() { "timestamp" => { - timestamp = map.next_value()?; + timestamp = parse_optional_number(&mut map)?; }, "end_timestamp" => { - end_timestamp = map.next_value()?; + end_timestamp = parse_optional_number(&mut map)?; }, "include_time" => { - include_time = map.next_value()?; + include_time = map.next_value().ok(); }, "is_range" => { - is_range = map.next_value()?; + is_range = map.next_value().ok(); }, "reminder_id" => { - reminder_id = map.next_value()?; + reminder_id = map.next_value().ok(); + }, + _ => { + let _: serde_json::Value = map.next_value()?; // Ignore unknown keys }, - _ => {}, } } - let include_time = include_time.unwrap_or_default(); - let is_range = is_range.unwrap_or_default(); - let reminder_id = reminder_id.unwrap_or_default(); - Ok(DateCellData { timestamp, end_timestamp, - include_time, - is_range, - reminder_id, + include_time: include_time.unwrap_or_default(), + is_range: is_range.unwrap_or_default(), + reminder_id: reminder_id.unwrap_or_default(), }) } } - deserializer.deserialize_any(DateCellVisitor()) + deserializer.deserialize_any(DateCellVisitor) } } +fn parse_optional_number<'de, M>(map: &mut M) -> core::result::Result, M::Error> +where + M: serde::de::MapAccess<'de>, +{ + match map.next_value::() { + Ok(serde_json::Value::Number(num)) => { + if let Some(int) = num.as_i64() { + Ok(Some(int)) + } else { + Ok(None) + } + }, + Ok(serde_json::Value::String(s)) => s.parse::().ok().map(Some).ok_or_else(|| { + serde::de::Error::custom(format!( + "Expected a numeric value or parsable string, got {}", + s + )) + }), + Ok(_) => Ok(None), + Err(_) => Ok(None), + } +} impl ToString for DateCellData { fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_date_cell_data_from_cell() { + let mut cell = Cell::new(); + cell.insert(CELL_DATA.into(), "1672531200".to_string().into()); // Timestamp for 2023-01-01T00:00:00Z + cell.insert("end_timestamp".into(), "1672617600".to_string().into()); // Timestamp for 2023-01-02T00:00:00Z + cell.insert("include_time".into(), true.into()); + cell.insert("is_range".into(), true.into()); + cell.insert("reminder_id".into(), "reminder123".to_string().into()); + + let date_cell_data = DateCellData::from(&cell); + assert_eq!(date_cell_data.timestamp, Some(1672531200)); + assert_eq!(date_cell_data.end_timestamp, Some(1672617600)); + assert!(date_cell_data.include_time); + assert!(date_cell_data.is_range); + assert_eq!(date_cell_data.reminder_id, "reminder123"); + } + + #[test] + fn test_date_cell_data_to_cell() { + let date_cell_data = DateCellData { + timestamp: Some(1672531200), + end_timestamp: Some(1672617600), + include_time: true, + is_range: true, + reminder_id: "reminder123".to_string(), + }; + + let cell = Cell::from(&date_cell_data); + assert_eq!( + cell.get_as::(CELL_DATA), + Some("1672531200".to_string()) + ); + assert_eq!( + cell.get_as::("end_timestamp"), + Some("1672617600".to_string()) + ); + assert_eq!(cell.get_as::("include_time"), Some(true)); + assert_eq!(cell.get_as::("is_range"), Some(true)); + assert_eq!( + cell.get_as::("reminder_id"), + Some("reminder123".to_string()) + ); + } + + #[test] + fn test_date_type_option_json_cell() { + let date_type_option = DateTypeOption::default_utc(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.into(), "1672531200".to_string().into()); + + let json_value = date_type_option.json_cell(&cell); + assert_eq!( + json_value, + json!({ + "timestamp": 1672531200, + "end_timestamp": null, + "include_time": false, + "is_range": false, + "reminder_id": "" + }) + ); + } + + #[test] + fn test_date_type_option_stringify_cell() { + let date_type_option = DateTypeOption::default_utc(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.into(), "1672531200".to_string().into()); + cell.insert("include_time".into(), true.into()); + + let result = date_type_option.stringify_cell(&cell); + assert_eq!(result, "Jan 01, 2023 00:00"); + } + + #[test] + fn test_date_type_option_numeric_cell() { + let date_type_option = DateTypeOption::default_utc(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.into(), "1672531200".to_string().into()); + + let result = date_type_option.numeric_cell(&cell); + assert_eq!(result, Some(1672531200.0)); + } + + #[test] + fn test_date_type_option_write_json() { + let date_type_option = DateTypeOption::default_utc(); + let json_value = json!({ + "timestamp": 1672531200, + "end_timestamp": 1672617600, + "include_time": true, + "is_range": true, + "reminder_id": "reminder123" + }); + + let cell = date_type_option.write_json(json_value); + assert_eq!( + cell.get_as::(CELL_DATA), + Some("1672531200".to_string()) + ); + assert_eq!( + cell.get_as::("end_timestamp"), + Some("1672617600".to_string()) + ); + assert_eq!(cell.get_as::("include_time"), Some(true)); + assert_eq!(cell.get_as::("is_range"), Some(true)); + assert_eq!( + cell.get_as::("reminder_id"), + Some("reminder123".to_string()) + ); + } + + #[test] + fn test_date_type_option_convert_raw_cell_data() { + let date_type_option = DateTypeOption::default_utc(); + + let raw_data = "1672531200"; + let result = date_type_option.convert_raw_cell_data(raw_data); + assert_eq!(result, "Jan 01, 2023"); + + let invalid_raw_data = "invalid"; + let result = date_type_option.convert_raw_cell_data(invalid_raw_data); + assert_eq!(result, ""); + } +} diff --git a/collab-database/src/fields/type_option/media_type_option.rs b/collab-database/src/fields/type_option/media_type_option.rs index 3040bf940..58e9346f1 100644 --- a/collab-database/src/fields/type_option/media_type_option.rs +++ b/collab-database/src/fields/type_option/media_type_option.rs @@ -1,10 +1,13 @@ use crate::database::gen_database_file_id; use crate::entity::FieldType; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; use crate::rows::{new_cell_builder, Cell}; use crate::template::entity::CELL_DATA; use collab::util::AnyMapExt; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::{json, Value}; use serde_repr::Serialize_repr; use std::fmt::{Display, Formatter}; use std::path::Path; @@ -24,15 +27,20 @@ impl Default for MediaTypeOption { } } -impl StringifyTypeOption for MediaTypeOption { - fn stringify_cell(&self, cell: &Cell) -> String { +impl TypeOptionCellReader for MediaTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { match cell.get_as::(CELL_DATA) { - None => "".to_string(), - Some(s) => s.to_string(), + None => Value::Null, + Some(s) => json!(s), } } - fn stringify_text(&self, text: &str) -> String { + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = cell.get_as::(CELL_DATA)?; + cell_data.parse::().ok() + } + + fn convert_raw_cell_data(&self, text: &str) -> String { let data = MediaCellData::from(text.to_string()); data .files @@ -43,6 +51,13 @@ impl StringifyTypeOption for MediaTypeOption { } } +impl TypeOptionCellWriter for MediaTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap_or_default(); + cell_data.into() + } +} + impl From for MediaTypeOption { fn from(data: TypeOptionData) -> Self { data @@ -59,7 +74,7 @@ impl From for TypeOptionData { } } -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct MediaCellData { pub files: Vec, } @@ -108,7 +123,7 @@ impl From<&Cell> for MediaCellData { impl From for Cell { fn from(value: MediaCellData) -> Self { let mut cell = new_cell_builder(FieldType::Media); - cell.insert(CELL_DATA.into(), value.into()); + cell.insert(CELL_DATA.into(), value.to_string().into()); cell } } @@ -342,22 +357,124 @@ impl<'de> Deserialize<'de> for MediaUploadType { #[cfg(test)] mod tests { use super::*; - use serde_json; + use serde_json::json; #[test] - fn test_serialize_deserialize_media_file() { - let media_file = MediaFile { - id: "123".to_string(), - name: "test_file".to_string(), - url: "http://example.com/file".to_string(), - upload_type: MediaUploadType::Cloud, - file_type: MediaFileType::Image, + fn test_media_cell_data_to_string() { + let media_file_1 = MediaFile::new( + "file1.jpg".to_string(), + "http://example.com/file1.jpg".to_string(), + MediaUploadType::Local, + MediaFileType::Image, + ); + let media_file_2 = MediaFile::new( + "file2.png".to_string(), + "http://example.com/file2.png".to_string(), + MediaUploadType::Cloud, + MediaFileType::Image, + ); + + let media_cell_data = MediaCellData { + files: vec![media_file_1.clone(), media_file_2.clone()], }; - // Serialize the MediaFile to a JSON string - let serialized = serde_json::to_string(&media_file).unwrap(); - println!("Serialized MediaFile: {}", serialized); - let deserialized: MediaFile = serde_json::from_str(&serialized).unwrap(); - assert_eq!(media_file, deserialized); + let expected = "file1.jpg, file2.png".to_string(); + assert_eq!(media_cell_data.to_string(), expected); + } + + #[test] + fn test_media_file_type_from_file_extension() { + assert_eq!( + MediaFileType::from_file("example.jpg"), + MediaFileType::Image + ); + assert_eq!( + MediaFileType::from_file("example.mp4"), + MediaFileType::Video + ); + assert_eq!( + MediaFileType::from_file("example.unknown"), + MediaFileType::Other + ); + } + + #[test] + fn test_serialize_deserialize_media_cell_data() { + let media_file_1 = MediaFile::new( + "file1.jpg".to_string(), + "http://example.com/file1.jpg".to_string(), + MediaUploadType::Local, + MediaFileType::Image, + ); + let media_file_2 = MediaFile::new( + "file2.png".to_string(), + "http://example.com/file2.png".to_string(), + MediaUploadType::Cloud, + MediaFileType::Image, + ); + + let media_cell_data = MediaCellData { + files: vec![media_file_1.clone(), media_file_2.clone()], + }; + + // Serialize to JSON + let serialized = serde_json::to_string(&media_cell_data).unwrap(); + println!("Serialized MediaCellData: {}", serialized); + + // Deserialize back to struct + let deserialized: MediaCellData = serde_json::from_str(&serialized).unwrap(); + assert_eq!(media_cell_data, deserialized); + } + + #[test] + fn test_media_file_display() { + let media_file = MediaFile::new( + "test_file.txt".to_string(), + "http://example.com/file.txt".to_string(), + MediaUploadType::Network, + MediaFileType::Text, + ); + + let expected_display = format!( + "MediaFile(id: {}, name: test_file.txt, url: http://example.com/file.txt, upload_type: {:?}, file_type: {:?})", + media_file.id, media_file.upload_type, media_file.file_type + ); + + assert_eq!(media_file.to_string(), expected_display); + } + + #[test] + fn test_deserialize_media_upload_type() { + let json_local = json!("Local"); + let json_network = json!(1); + let json_cloud = json!("CloudMedia"); + + assert_eq!( + serde_json::from_value::(json_local).unwrap(), + MediaUploadType::Local + ); + assert_eq!( + serde_json::from_value::(json_network).unwrap(), + MediaUploadType::Network + ); + assert_eq!( + serde_json::from_value::(json_cloud).unwrap(), + MediaUploadType::Cloud + ); + } + + #[test] + fn test_deserialize_media_file_type() { + let json_image = json!(1); + let json_text = json!("Text"); + + assert_eq!( + serde_json::from_value::(json_image).unwrap(), + MediaFileType::Image + ); + assert_eq!( + serde_json::from_value::(json_text).unwrap(), + MediaFileType::Text + ); } } diff --git a/collab-database/src/fields/type_option/mod.rs b/collab-database/src/fields/type_option/mod.rs index f7700bc8c..a78927515 100644 --- a/collab-database/src/fields/type_option/mod.rs +++ b/collab-database/src/fields/type_option/mod.rs @@ -15,10 +15,15 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use crate::entity::FieldType; +use crate::fields::checklist_type_option::ChecklistTypeOption; use crate::fields::date_type_option::{DateTypeOption, TimeTypeOption}; use crate::fields::media_type_option::MediaTypeOption; use crate::fields::number_type_option::NumberTypeOption; +use crate::fields::relation_type_option::RelationTypeOption; use crate::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::fields::summary_type_option::SummarizationTypeOption; +use crate::fields::timestamp_type_option::TimestampTypeOption; +use crate::fields::translate_type_option::TranslateTypeOption; use crate::fields::type_option::checkbox_type_option::CheckboxTypeOption; use crate::fields::type_option::text_type_option::RichTextTypeOption; use crate::fields::url_type_option::URLTypeOption; @@ -114,20 +119,49 @@ pub type TypeOptionData = HashMap; pub type TypeOptionDataBuilder = HashMap; pub type TypeOptionUpdate = MapRef; -/// It's used to parse each cell into readable text -pub trait StringifyTypeOption { +/// [TypeOptionCellReader] is a trait that provides methods to read cell data based on the field type. +/// It's used to convert the raw cell data into a human-readable text representation. +pub trait TypeOptionCellReader { + /// Returns the cell data as a JSON value. + /// + /// The type of the returned value depends on the field type: + /// - **Single Select**: Returns an array of [SelectOption]. + /// - **RichText**: Returns a string. + /// - Other field types: Returns appropriate JSON values, such as objects or arrays. + fn json_cell(&self, cell: &Cell) -> serde_json::Value; + + /// Returns a human-readable text representation of the cell. + /// + /// For certain field types, the raw cell data might require formatting: + /// - **Single/Multi-Select**: The raw data may contain IDs as a comma-separated string. + /// Calling `stringify_cell` will convert these IDs into a list of option names, separated by commas. fn stringify_cell(&self, cell: &Cell) -> String { match cell.get_as::(CELL_DATA) { None => "".to_string(), - Some(s) => Self::stringify_text(self, &s), + Some(s) => Self::convert_raw_cell_data(self, &s), } } - fn stringify_text(&self, text: &str) -> String; + + /// Returns the numeric value of the cell. If the value is not numeric, returns `None`. + fn numeric_cell(&self, cell: &Cell) -> Option; + + /// Convert the value stored in given key:[CELL_DATA] into a readable text + fn convert_raw_cell_data(&self, cell_data: &str) -> String; } -pub fn stringify_type_option( + +/// [TypeOptionCellWriter] is a trait that provides methods to write [serde_json::Value] into a cell. +/// Different field types have their own implementation about how to convert [serde_json::Value] into [Cell]. +pub trait TypeOptionCellWriter { + /// Write json value into a cell + /// Different type option has its own implementation about how to convert [serde_json::Value] + /// into [Cell] + fn write_json(&self, json_value: serde_json::Value) -> Cell; +} + +pub fn type_option_cell_writer( type_option_data: TypeOptionData, field_type: &FieldType, -) -> Option> { +) -> Option> { match field_type { FieldType::RichText => Some(Box::new(RichTextTypeOption::from(type_option_data))), FieldType::Number => Some(Box::new(NumberTypeOption::from(type_option_data))), @@ -138,12 +172,34 @@ pub fn stringify_type_option( FieldType::URL => Some(Box::new(URLTypeOption::from(type_option_data))), FieldType::Time => Some(Box::new(TimeTypeOption::from(type_option_data))), FieldType::Media => Some(Box::new(MediaTypeOption::from(type_option_data))), + FieldType::Checklist => Some(Box::new(ChecklistTypeOption::from(type_option_data))), + FieldType::LastEditedTime => Some(Box::new(TimestampTypeOption::from(type_option_data))), + FieldType::CreatedTime => Some(Box::new(TimestampTypeOption::from(type_option_data))), + FieldType::Relation => Some(Box::new(RelationTypeOption::from(type_option_data))), + FieldType::Summary => Some(Box::new(SummarizationTypeOption::from(type_option_data))), + FieldType::Translate => Some(Box::new(TranslateTypeOption::from(type_option_data))), + } +} - FieldType::Checklist - | FieldType::LastEditedTime - | FieldType::CreatedTime - | FieldType::Relation - | FieldType::Summary - | FieldType::Translate => None, +pub fn type_option_cell_reader( + type_option_data: TypeOptionData, + field_type: &FieldType, +) -> Option> { + match field_type { + FieldType::RichText => Some(Box::new(RichTextTypeOption::from(type_option_data))), + FieldType::Number => Some(Box::new(NumberTypeOption::from(type_option_data))), + FieldType::DateTime => Some(Box::new(DateTypeOption::from(type_option_data))), + FieldType::SingleSelect => Some(Box::new(SingleSelectTypeOption::from(type_option_data))), + FieldType::MultiSelect => Some(Box::new(MultiSelectTypeOption::from(type_option_data))), + FieldType::Checkbox => Some(Box::new(CheckboxTypeOption::from(type_option_data))), + FieldType::URL => Some(Box::new(URLTypeOption::from(type_option_data))), + FieldType::Time => Some(Box::new(TimeTypeOption::from(type_option_data))), + FieldType::Media => Some(Box::new(MediaTypeOption::from(type_option_data))), + FieldType::Checklist => Some(Box::new(ChecklistTypeOption::from(type_option_data))), + FieldType::LastEditedTime => Some(Box::new(TimestampTypeOption::from(type_option_data))), + FieldType::CreatedTime => Some(Box::new(TimestampTypeOption::from(type_option_data))), + FieldType::Relation => Some(Box::new(RelationTypeOption::from(type_option_data))), + FieldType::Summary => Some(Box::new(SummarizationTypeOption::from(type_option_data))), + FieldType::Translate => Some(Box::new(TranslateTypeOption::from(type_option_data))), } } diff --git a/collab-database/src/fields/type_option/number_type_option.rs b/collab-database/src/fields/type_option/number_type_option.rs index 4e83ca312..280cdbc37 100644 --- a/collab-database/src/fields/type_option/number_type_option.rs +++ b/collab-database/src/fields/type_option/number_type_option.rs @@ -2,15 +2,22 @@ use crate::error::DatabaseError; use crate::fields::number_type_option::number_currency::Currency; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; use collab::preclude::Any; +use crate::entity::FieldType; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; use fancy_regex::Regex; use lazy_static::lazy_static; use rust_decimal::Decimal; use rusty_money::{define_currency_set, Money}; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use std::str::FromStr; use strum::IntoEnumIterator; use strum_macros::EnumIter; @@ -58,8 +65,18 @@ impl From for TypeOptionData { } } -impl StringifyTypeOption for NumberTypeOption { - fn stringify_text(&self, text: &str) -> String { +impl TypeOptionCellReader for NumberTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + // Returns the formated number string. + Value::String(self.stringify_cell(cell)) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = cell.get_as::(CELL_DATA)?; + cell_data.parse::().ok() + } + + fn convert_raw_cell_data(&self, text: &str) -> String { match self.format_cell_data(text) { Ok(cell_data) => cell_data.to_string(), Err(_) => "".to_string(), @@ -67,6 +84,20 @@ impl StringifyTypeOption for NumberTypeOption { } } +impl TypeOptionCellWriter for NumberTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::Number); + if let Some(data) = match json_value { + Value::String(s) => Some(s), + Value::Number(n) => Some(n.to_string()), + _ => None, + } { + cell.insert(CELL_DATA.into(), data.into()); + } + cell + } +} + impl NumberTypeOption { pub fn new() -> Self { Self::default() @@ -797,7 +828,7 @@ mod tests { } fn assert_number(type_option: &NumberTypeOption, input_str: &str, expected_str: &str) { - let output = type_option.stringify_text(input_str); + let output = type_option.convert_raw_cell_data(input_str); assert_eq!(output, expected_str.to_owned()); } } diff --git a/collab-database/src/fields/type_option/relation_type_option.rs b/collab-database/src/fields/type_option/relation_type_option.rs index 97ebb5e38..72bd32fb3 100644 --- a/collab-database/src/fields/type_option/relation_type_option.rs +++ b/collab-database/src/fields/type_option/relation_type_option.rs @@ -1,7 +1,10 @@ +use super::{TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{TypeOptionCellReader, TypeOptionCellWriter}; +use crate::rows::Cell; +use crate::template::relation_parse::RelationCellData; use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; - -use super::{TypeOptionData, TypeOptionDataBuilder}; +use serde_json::{json, Value}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RelationTypeOption { @@ -20,3 +23,31 @@ impl From for TypeOptionData { TypeOptionDataBuilder::from([("database_id".into(), data.database_id.into())]) } } + +impl TypeOptionCellReader for RelationTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + let cell_data = RelationCellData::from(cell); + json!(cell_data) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, cell_data: &str) -> String { + let cell_data = RelationCellData::from(cell_data); + cell_data + .row_ids + .into_iter() + .map(|id| id.to_string()) + .collect::>() + .join(", ") + } +} + +impl TypeOptionCellWriter for RelationTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap_or_default(); + Cell::from(&cell_data) + } +} diff --git a/collab-database/src/fields/type_option/select_type_option.rs b/collab-database/src/fields/type_option/select_type_option.rs index 4adff0ec5..4600531ec 100644 --- a/collab-database/src/fields/type_option/select_type_option.rs +++ b/collab-database/src/fields/type_option/select_type_option.rs @@ -1,22 +1,56 @@ use crate::database::gen_option_id; +use crate::entity::FieldType; use crate::error::DatabaseError; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; use crate::rows::{new_cell_builder, Cell}; use crate::template::entity::CELL_DATA; use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SelectTypeOption { pub options: Vec, + #[serde(default)] pub disable_color: bool, } -impl StringifyTypeOption for SelectTypeOption { - fn stringify_text(&self, text: &str) -> String { +impl TypeOptionCellReader for SelectTypeOption { + /// Returns list of selected options + fn json_cell(&self, cell: &Cell) -> Value { + match cell.get_as::(CELL_DATA) { + None => Value::Null, + Some(s) => { + let ids = SelectOptionIds::from_str(&s).unwrap_or_default().0; + if ids.is_empty() { + return Value::Array(vec![]); + } + + let options = ids + .iter() + .flat_map(|option_id| { + self + .options + .iter() + .find(|option| &option.id == option_id) + .cloned() + }) + .collect::>(); + json!(options) + }, + } + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, text: &str) -> String { let ids = SelectOptionIds::from_str(text).unwrap_or_default().0; if ids.is_empty() { return "".to_string(); @@ -62,8 +96,10 @@ impl From for TypeOptionData { pub struct SelectOption { pub id: String, pub name: String, + #[serde(default)] pub color: SelectOptionColor, } + impl SelectOption { pub fn new(name: &str) -> Self { SelectOption { @@ -82,6 +118,7 @@ impl SelectOption { } } #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[serde(try_from = "u8", into = "u8")] #[repr(u8)] #[derive(Default)] pub enum SelectOptionColor { @@ -97,6 +134,31 @@ pub enum SelectOptionColor { Blue = 8, } +impl TryFrom for SelectOptionColor { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(SelectOptionColor::Purple), + 1 => Ok(SelectOptionColor::Pink), + 2 => Ok(SelectOptionColor::LightPink), + 3 => Ok(SelectOptionColor::Orange), + 4 => Ok(SelectOptionColor::Yellow), + 5 => Ok(SelectOptionColor::Lime), + 6 => Ok(SelectOptionColor::Green), + 7 => Ok(SelectOptionColor::Aqua), + 8 => Ok(SelectOptionColor::Blue), + _ => Err("Invalid color value"), + } + } +} + +impl From for u8 { + fn from(color: SelectOptionColor) -> Self { + color as u8 + } +} + impl From for SelectOptionColor { fn from(index: usize) -> Self { match index { @@ -117,9 +179,23 @@ impl From for SelectOptionColor { #[derive(Clone, Default, Debug)] pub struct SingleSelectTypeOption(pub SelectTypeOption); -impl StringifyTypeOption for SingleSelectTypeOption { - fn stringify_text(&self, text: &str) -> String { - self.0.stringify_text(text) +impl TypeOptionCellReader for SingleSelectTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + self.0.json_cell(cell) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + self.0.numeric_cell(cell) + } + + fn convert_raw_cell_data(&self, text: &str) -> String { + self.0.convert_raw_cell_data(text) + } +} + +impl TypeOptionCellWriter for SingleSelectTypeOption { + fn write_json(&self, value: Value) -> Cell { + cell_from_json_value(value, &self.options, FieldType::SingleSelect) } } @@ -152,9 +228,23 @@ impl From for TypeOptionData { // Multiple select #[derive(Clone, Default, Debug)] pub struct MultiSelectTypeOption(pub SelectTypeOption); -impl StringifyTypeOption for MultiSelectTypeOption { - fn stringify_text(&self, text: &str) -> String { - self.0.stringify_text(text) +impl TypeOptionCellReader for MultiSelectTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + self.0.json_cell(cell) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + self.0.numeric_cell(cell) + } + + fn convert_raw_cell_data(&self, text: &str) -> String { + self.0.convert_raw_cell_data(text) + } +} + +impl TypeOptionCellWriter for MultiSelectTypeOption { + fn write_json(&self, value: Value) -> Cell { + cell_from_json_value(value, &self.options, FieldType::MultiSelect) } } @@ -193,7 +283,7 @@ impl SelectOptionIds { pub fn into_inner(self) -> Vec { self.0 } - pub fn to_cell_data(&self, field_type: impl Into) -> Cell { + pub fn to_cell(&self, field_type: impl Into) -> Cell { let mut cell = new_cell_builder(field_type); cell.insert(CELL_DATA.into(), self.to_string().into()); cell @@ -255,3 +345,204 @@ impl std::ops::DerefMut for SelectOptionIds { &mut self.0 } } +fn cell_from_json_value(value: Value, options: &[SelectOption], field_type: FieldType) -> Cell { + match value { + Value::Array(array) => { + // Process array of JSON objects or strings + let ids = array + .iter() + .filter_map(|item| match item { + Value::Object(obj) => obj + .get("id") + .and_then(|v| v.as_str()) + .map(String::from) + .or_else(|| { + obj.get("name").and_then(|v| v.as_str()).and_then(|name| { + options + .iter() + .find(|opt| opt.name == name) + .map(|opt| opt.id.clone()) + }) + }), + Value::String(name) => options + .iter() + .find(|opt| &opt.name == name) + .map(|opt| opt.id.clone()), + _ => None, + }) + .collect::>(); + + SelectOptionIds::from(ids).to_cell(field_type) + }, + + Value::String(s) => { + // Process a single string (comma-separated names or IDs) + let ids = s + .split(SELECTION_IDS_SEPARATOR) + .map(str::trim) + .filter_map(|name| { + options + .iter() + .find(|opt| opt.name == name) + .map(|opt| opt.id.clone()) + }) + .collect::>(); + + SelectOptionIds::from(ids).to_cell(field_type) + }, + Value::Object(obj) => { + // Process a single object with "id" or "name" + if let Some(id) = obj.get("id").and_then(|v| v.as_str()) { + if options.iter().any(|opt| opt.id == id) { + return SelectOptionIds::from(vec![id.to_string()]).to_cell(field_type); + } + } + + if let Some(name) = obj.get("name").and_then(|v| v.as_str()) { + if let Some(option) = options.iter().find(|opt| opt.name == name) { + return SelectOptionIds::from(vec![option.id.clone()]).to_cell(field_type); + } + } + SelectOptionIds::new().to_cell(field_type) + }, + _ => SelectOptionIds::new().to_cell(field_type), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{json, Value}; + + #[test] + fn test_serialize_deserialize_select_type_option() { + let options = vec![ + SelectOption::new("Option 1"), + SelectOption::with_color("Option 2", SelectOptionColor::Blue), + ]; + + let select_type_option = SelectTypeOption { + options, + disable_color: false, + }; + + let serialized = serde_json::to_string(&select_type_option).unwrap(); + let deserialized: SelectTypeOption = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(select_type_option.disable_color, deserialized.disable_color); + assert_eq!(select_type_option.options.len(), deserialized.options.len()); + assert_eq!(select_type_option.options[0].name, "Option 1"); + assert_eq!(select_type_option.options[1].color, SelectOptionColor::Blue); + } + + #[test] + fn test_select_option_ids_to_string() { + let ids = SelectOptionIds::from(vec!["id1".to_string(), "id2".to_string()]); + assert_eq!(ids.to_string(), "id1,id2"); + } + + #[test] + fn test_select_option_ids_from_str() { + let ids = SelectOptionIds::from_str("id1,id2").unwrap(); + assert_eq!(ids.0, vec!["id1".to_string(), "id2".to_string()]); + } + + #[test] + fn test_cell_from_json_value_array() { + let options = vec![SelectOption::new("Option 1"), SelectOption::new("Option 2")]; + + let value = json!([ + { "id": options[0].id }, + { "name": "Option 2" } + ]); + + let cell = cell_from_json_value(value, &options, FieldType::MultiSelect); + let cell_data: String = cell.get_as(CELL_DATA).unwrap(); + assert!(cell_data.contains(&options[0].id)); + assert!(cell_data.contains(&options[1].id)); + } + + #[test] + fn test_cell_from_json_value_string() { + let options = vec![SelectOption::new("Option 1"), SelectOption::new("Option 2")]; + + let value = Value::String("Option 1,Option 2".to_string()); + let cell = cell_from_json_value(value, &options, FieldType::MultiSelect); + + let cell_data: String = cell.get_as(CELL_DATA).unwrap(); + assert!(cell_data.contains(&options[0].id)); + assert!(cell_data.contains(&options[1].id)); + } + + #[test] + fn test_single_select_type_option_write_json() { + let options = vec![SelectOption::new("Option A"), SelectOption::new("Option B")]; + let single_select = SingleSelectTypeOption(SelectTypeOption { + options, + disable_color: false, + }); + + let json_value = json!({ "name": "Option A" }); + let cell = single_select.write_json(json_value); + + let cell_data: String = cell.get_as(CELL_DATA).unwrap(); + assert!(!cell_data.is_empty()); + } + + #[test] + fn test_multi_select_type_option_write_json() { + let options = vec![SelectOption::new("Option 1"), SelectOption::new("Option 2")]; + let multi_select = MultiSelectTypeOption(SelectTypeOption { + options, + disable_color: false, + }); + + let json_value = json!([ + { "name": "Option 1" }, + { "id": multi_select.options[1].id } + ]); + + let cell = multi_select.write_json(json_value); + let cell_data: String = cell.get_as(CELL_DATA).unwrap(); + assert!(cell_data.contains(&multi_select.options[0].id)); + assert!(cell_data.contains(&multi_select.options[1].id)); + } + + #[test] + fn test_select_option_with_color() { + let option = SelectOption::with_color("Colored Option", SelectOptionColor::Aqua); + assert_eq!(option.name, "Colored Option"); + assert_eq!(option.color, SelectOptionColor::Aqua); + } + + #[test] + fn test_select_option_color_from_u8() { + assert_eq!( + SelectOptionColor::try_from(0_u8).unwrap(), + SelectOptionColor::Purple + ); + assert_eq!( + SelectOptionColor::try_from(8_u8).unwrap(), + SelectOptionColor::Blue + ); + assert!(SelectOptionColor::try_from(10_u8).is_err()); + } + + #[test] + fn test_convert_raw_cell_data() { + let options = vec![SelectOption::new("Option 1"), SelectOption::new("Option 2")]; + let raw_data = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(","); + + let select_type_option = SelectTypeOption { + options, + disable_color: false, + }; + + let result = select_type_option.convert_raw_cell_data(&raw_data); + assert_eq!(result, "Option 1, Option 2"); + } +} diff --git a/collab-database/src/fields/type_option/summary_type_option.rs b/collab-database/src/fields/type_option/summary_type_option.rs index 90e76e164..d1e546344 100644 --- a/collab-database/src/fields/type_option/summary_type_option.rs +++ b/collab-database/src/fields/type_option/summary_type_option.rs @@ -1,7 +1,10 @@ +use super::{TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{TypeOptionCellReader, TypeOptionCellWriter}; +use crate::rows::Cell; +use crate::template::summary_parse::SummaryCellData; use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; - -use super::{TypeOptionData, TypeOptionDataBuilder}; +use serde_json::{json, Value}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SummarizationTypeOption { @@ -20,3 +23,26 @@ impl From for TypeOptionData { TypeOptionDataBuilder::from([("auto_fill".into(), data.auto_fill.into())]) } } + +impl TypeOptionCellReader for SummarizationTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + let cell_data = SummaryCellData::from(cell); + json!(cell_data) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, cell_data: &str) -> String { + let cell_data = SummaryCellData(cell_data.to_string()); + cell_data.to_string() + } +} + +impl TypeOptionCellWriter for SummarizationTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell_data = serde_json::from_value::(json_value).unwrap_or_default(); + cell_data.into() + } +} diff --git a/collab-database/src/fields/type_option/text_type_option.rs b/collab-database/src/fields/type_option/text_type_option.rs index c5cf93fac..9c311b2b6 100644 --- a/collab-database/src/fields/type_option/text_type_option.rs +++ b/collab-database/src/fields/type_option/text_type_option.rs @@ -1,15 +1,37 @@ -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::entity::FieldType; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RichTextTypeOption; -impl StringifyTypeOption for RichTextTypeOption { - fn stringify_text(&self, text: &str) -> String { +impl TypeOptionCellReader for RichTextTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + json!(self.stringify_cell(cell)) + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + self.stringify_cell(cell).parse().ok() + } + + fn convert_raw_cell_data(&self, text: &str) -> String { text.to_string() } } +impl TypeOptionCellWriter for RichTextTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::RichText); + cell.insert(CELL_DATA.into(), json_value.to_string().into()); + cell + } +} + impl From for RichTextTypeOption { fn from(_data: TypeOptionData) -> Self { RichTextTypeOption diff --git a/collab-database/src/fields/type_option/timestamp_type_option.rs b/collab-database/src/fields/type_option/timestamp_type_option.rs index ca59ce1f4..d0c938f00 100644 --- a/collab-database/src/fields/type_option/timestamp_type_option.rs +++ b/collab-database/src/fields/type_option/timestamp_type_option.rs @@ -1,9 +1,16 @@ use crate::entity::FieldType; use crate::fields::date_type_option::{DateFormat, TimeFormat}; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; -use chrono::{DateTime, Local, Offset}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use chrono::{DateTime, Local, Offset, TimeZone}; +use chrono_tz::Tz; use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::str::FromStr; use yrs::Any; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -12,10 +19,22 @@ pub struct TimestampTypeOption { pub time_format: TimeFormat, pub include_time: bool, pub field_type: i64, + #[serde(default)] + pub timezone: Option, } -impl StringifyTypeOption for TimestampTypeOption { - fn stringify_text(&self, text: &str) -> String { +impl TypeOptionCellReader for TimestampTypeOption { + /// Return formated date and time string for the cell + fn json_cell(&self, cell: &Cell) -> Value { + let s = self.stringify_cell(cell); + json!(s) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, text: &str) -> String { let (date_string, time_string) = self.formatted_date_time_from_timestamp(&text.parse::().ok()); if self.include_time { @@ -25,6 +44,21 @@ impl StringifyTypeOption for TimestampTypeOption { } } } + +impl TypeOptionCellWriter for TimestampTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::Time); + if let Some(data) = match json_value { + Value::String(s) => s.parse::().ok(), + Value::Number(n) => n.as_i64(), + _ => None, + } { + cell.insert(CELL_DATA.into(), data.into()); + } + cell + } +} + impl TimestampTypeOption { pub fn new>(field_type: T) -> Self { Self { @@ -38,9 +72,14 @@ impl TimestampTypeOption { if let Some(naive) = timestamp.and_then(|timestamp| { chrono::DateTime::from_timestamp(timestamp, 0).map(|date| date.naive_utc()) }) { - let offset = Local::now().offset().fix(); - let date_time = DateTime::::from_naive_utc_and_offset(naive, offset); + let offset = self + .timezone + .as_ref() + .and_then(|timezone| Tz::from_str(timezone).ok()) + .map(|tz| tz.offset_from_utc_datetime(&naive).fix()) + .unwrap_or_else(|| Local::now().offset().fix()); + let date_time = DateTime::::from_naive_utc_and_offset(naive, offset); let fmt = self.date_format.format_str(); let date = format!("{}", date_time.format(fmt)); let fmt = self.time_format.format_str(); @@ -59,6 +98,7 @@ impl Default for TimestampTypeOption { time_format: Default::default(), include_time: true, field_type: FieldType::LastEditedTime.into(), + timezone: None, } } } @@ -79,11 +119,13 @@ impl From for TimestampTypeOption { .map(FieldType::from) .unwrap_or(FieldType::LastEditedTime) .into(); + let timezone = data.get_as::("timezone"); Self { date_format, time_format, include_time, field_type, + timezone, } } } @@ -104,3 +146,142 @@ impl From for TypeOptionData { ]) } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::i64; + + #[test] + fn test_default_timestamp_type_option() { + let default_option = TimestampTypeOption::default(); + assert!(default_option.include_time); + assert_eq!( + default_option.field_type, + i64::from(FieldType::LastEditedTime) + ); + } + + #[test] + fn test_from_type_option_data() { + let data = TypeOptionDataBuilder::from([ + ("date_format".into(), Any::BigInt(2)), + ("time_format".into(), Any::BigInt(1)), + ("include_time".into(), Any::Bool(false)), + ( + "field_type".into(), + Any::BigInt(FieldType::CreatedTime.into()), + ), + ]); + + let option = TimestampTypeOption::from(data); + assert_eq!(option.date_format, DateFormat::ISO); + assert_eq!(option.time_format, TimeFormat::TwentyFourHour); + assert!(!option.include_time); + assert_eq!(option.field_type, i64::from(FieldType::CreatedTime)); + } + + #[test] + fn test_into_type_option_data() { + let option = TimestampTypeOption { + date_format: DateFormat::Friendly, + time_format: TimeFormat::TwelveHour, + include_time: true, + field_type: FieldType::CreatedTime.into(), + timezone: None, + }; + + let data: TypeOptionData = option.into(); + assert_eq!(data.get_as::("date_format"), Some(3)); // Friendly format + assert_eq!(data.get_as::("time_format"), Some(0)); // TwelveHour format + assert_eq!(data.get_as::("include_time"), Some(true)); + assert_eq!( + data.get_as::("field_type"), + Some(i64::from(FieldType::CreatedTime)) + ); + } + + #[test] + fn test_formatted_date_time_from_timestamp() { + let option = TimestampTypeOption { + date_format: DateFormat::Friendly, + time_format: TimeFormat::TwentyFourHour, + include_time: true, + field_type: FieldType::CreatedTime.into(), + timezone: Some("Etc/UTC".to_string()), + }; + + let timestamp = Some(1672531200); // January 1, 2023 00:00:00 UTC + let (date, time) = option.formatted_date_time_from_timestamp(×tamp); + + assert_eq!(date, "Jan 01, 2023"); + assert_eq!(time, "00:00"); + } + + #[test] + fn test_json_cell() { + let option = TimestampTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: true, + field_type: FieldType::CreatedTime.into(), + timezone: Some("Etc/UTC".to_string()), + }; + + let mut cell = Cell::new(); + cell.insert(CELL_DATA.into(), 1672531200.to_string().into()); // January 1, 2023 00:00:00 UTC + + let json_value = option.json_cell(&cell); + assert_eq!(json_value, json!("2023/01/01 00:00")); + } + + #[test] + fn test_convert_raw_cell_data() { + let option = TimestampTypeOption { + date_format: DateFormat::ISO, + time_format: TimeFormat::TwentyFourHour, + include_time: false, + field_type: FieldType::CreatedTime.into(), + timezone: None, + }; + + let raw_data = "1672531200"; // January 1, 2023 00:00:00 UTC + let result = option.convert_raw_cell_data(raw_data); + + assert_eq!(result, "2023-01-01"); + } + + #[test] + fn test_write_json_valid_number() { + let option = TimestampTypeOption::default(); + let json_value = json!(1672531200); + + let cell = option.write_json(json_value); + let data: i64 = cell.get_as(CELL_DATA).unwrap(); + + assert_eq!(data, 1672531200); + } + + #[test] + fn test_write_json_valid_string() { + let option = TimestampTypeOption::default(); + let json_value = json!("1672531200"); + + let cell = option.write_json(json_value); + let data: i64 = cell.get_as(CELL_DATA).unwrap(); + + assert_eq!(data, 1672531200); + } + + #[test] + fn test_write_json_invalid_data() { + let option = TimestampTypeOption::default(); + let json_value = json!("invalid"); + + let cell = option.write_json(json_value); + let data: Option = cell.get_as(CELL_DATA); + + assert!(data.is_none()); + } +} diff --git a/collab-database/src/fields/type_option/translate_type_option.rs b/collab-database/src/fields/type_option/translate_type_option.rs index a53ddbab4..5f9198883 100644 --- a/collab-database/src/fields/type_option/translate_type_option.rs +++ b/collab-database/src/fields/type_option/translate_type_option.rs @@ -1,8 +1,11 @@ +use super::{TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{TypeOptionCellReader, TypeOptionCellWriter}; +use crate::rows::Cell; +use crate::template::translate_parse::TranslateCellData; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; use yrs::{encoding::serde::from_any, Any}; -use super::{TypeOptionData, TypeOptionDataBuilder}; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TranslateTypeOption { #[serde(default)] @@ -29,6 +32,28 @@ impl TranslateTypeOption { } } +impl TypeOptionCellReader for TranslateTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + json!(self.stringify_cell(cell)) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, cell_data: &str) -> String { + let cell = serde_json::from_str::(cell_data).unwrap_or_default(); + cell.to_string() + } +} + +impl TypeOptionCellWriter for TranslateTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let cell = TranslateCellData(json_value.as_str().unwrap_or_default().to_string()); + cell.into() + } +} + impl Default for TranslateTypeOption { fn default() -> Self { Self { diff --git a/collab-database/src/fields/type_option/url_type_option.rs b/collab-database/src/fields/type_option/url_type_option.rs index 90dd78220..a0243af10 100644 --- a/collab-database/src/fields/type_option/url_type_option.rs +++ b/collab-database/src/fields/type_option/url_type_option.rs @@ -1,11 +1,14 @@ use crate::entity::FieldType; use crate::error::DatabaseError; -use crate::fields::{StringifyTypeOption, TypeOptionData, TypeOptionDataBuilder}; +use crate::fields::{ + TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData, TypeOptionDataBuilder, +}; use crate::rows::{new_cell_builder, Cell}; use crate::template::entity::CELL_DATA; use collab::preclude::Any; use collab::util::AnyMapExt; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; use yrs::encoding::serde::from_any; #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -16,9 +19,26 @@ pub struct URLTypeOption { pub content: String, } -impl StringifyTypeOption for URLTypeOption { - fn stringify_text(&self, text: &str) -> String { - text.to_string() +impl TypeOptionCellReader for URLTypeOption { + fn json_cell(&self, cell: &Cell) -> Value { + json!(self.stringify_cell(cell)) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } + + fn convert_raw_cell_data(&self, text: &str) -> String { + let cell_data = URLCellData::new(text); + cell_data.to_string() + } +} + +impl TypeOptionCellWriter for URLTypeOption { + fn write_json(&self, json_value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::URL); + cell.insert(CELL_DATA.into(), json_value.to_string().into()); + cell } } diff --git a/collab-database/src/template/builder.rs b/collab-database/src/template/builder.rs index 56283389e..6afb292e5 100644 --- a/collab-database/src/template/builder.rs +++ b/collab-database/src/template/builder.rs @@ -12,7 +12,7 @@ use crate::fields::select_type_option::SelectTypeOption; use crate::fields::text_type_option::RichTextTypeOption; use crate::fields::timestamp_type_option::TimestampTypeOption; use crate::rows::new_cell_builder; -use crate::template::chect_list_parse::ChecklistCellData; +use crate::template::check_list_parse::ChecklistCellData; use crate::template::csv::CSVResource; use crate::template::date_parse::replace_cells_with_timestamp; use crate::template::media_parse::replace_cells_with_files; diff --git a/collab-database/src/template/check_list_parse.rs b/collab-database/src/template/check_list_parse.rs new file mode 100644 index 000000000..d961a765a --- /dev/null +++ b/collab-database/src/template/check_list_parse.rs @@ -0,0 +1,89 @@ +use crate::database::gen_option_id; +use crate::fields::select_type_option::{SelectOption, SelectOptionColor}; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct ChecklistCellData { + pub options: Vec, + #[serde(default)] + pub selected_option_ids: Vec, +} + +impl From<(Vec, Vec)> for ChecklistCellData { + fn from((names, selected_names): (Vec, Vec)) -> Self { + let options: Vec = names + .into_iter() + .enumerate() + .map(|(index, name)| SelectOption { + id: gen_option_id(), + name: name.clone(), + color: SelectOptionColor::from(index % 8), + }) + .collect(); + + let selected_option_ids: Vec = selected_names + .into_iter() + .map(|name| { + options + .iter() + .find(|opt| opt.name == name) + .map_or_else(gen_option_id, |opt| opt.id.clone()) + }) + .collect(); + + ChecklistCellData { + options, + selected_option_ids, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rows::Cell; + + #[test] + fn test_checklist_cell_data_from_names_and_selected() { + let names = vec![ + "Option 1".to_string(), + "Option 2".to_string(), + "Option 3".to_string(), + ]; + let selected_names = vec!["Option 1".to_string(), "Option 3".to_string()]; + let checklist_data = ChecklistCellData::from((names, selected_names)); + + assert_eq!(checklist_data.options.len(), 3); + assert_eq!(checklist_data.selected_option_ids.len(), 2); + + let selected_names_set: Vec<_> = checklist_data + .selected_option_ids + .iter() + .filter_map(|id| { + checklist_data + .options + .iter() + .find(|opt| opt.id == *id) + .map(|opt| &opt.name) + }) + .collect(); + + assert_eq!(selected_names_set, vec!["Option 1", "Option 3"]); + } + + #[test] + fn test_checklist_cell_data_to_and_from_cell() { + let names = vec!["Option A".to_string(), "Option B".to_string()]; + let selected_names = vec!["Option A".to_string()]; + let checklist_data = ChecklistCellData::from((names.clone(), selected_names.clone())); + + let cell: Cell = Cell::from(checklist_data.clone()); + let restored_data = ChecklistCellData::from(&cell); + + assert_eq!(restored_data.options.len(), checklist_data.options.len()); + assert_eq!( + restored_data.selected_option_ids, + checklist_data.selected_option_ids + ); + } +} diff --git a/collab-database/src/template/chect_list_parse.rs b/collab-database/src/template/chect_list_parse.rs deleted file mode 100644 index ad9f8348a..000000000 --- a/collab-database/src/template/chect_list_parse.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::database::gen_option_id; -use crate::fields::select_type_option::{SelectOption, SelectOptionColor}; -use serde::{Deserialize, Serialize}; - -#[derive(Default, Clone, Serialize, Deserialize)] -pub struct ChecklistCellData { - pub options: Vec, - pub selected_option_ids: Vec, -} - -impl From<(Vec, Vec)> for ChecklistCellData { - fn from((names, selected_names): (Vec, Vec)) -> Self { - let options: Vec = names - .into_iter() - .enumerate() - .map(|(index, name)| SelectOption { - id: gen_option_id(), - name: name.clone(), - color: SelectOptionColor::from(index % 8), - }) - .collect(); - - let selected_option_ids: Vec = selected_names - .into_iter() - .map(|name| { - options - .iter() - .find(|opt| opt.name == name) - .map_or_else(gen_option_id, |opt| opt.id.clone()) - }) - .collect(); - - ChecklistCellData { - options, - selected_option_ids, - } - } -} diff --git a/collab-database/src/template/mod.rs b/collab-database/src/template/mod.rs index 9581f2b47..96068559e 100644 --- a/collab-database/src/template/mod.rs +++ b/collab-database/src/template/mod.rs @@ -1,8 +1,12 @@ pub mod builder; -mod chect_list_parse; +pub mod check_list_parse; pub mod csv; pub mod date_parse; pub mod entity; mod media_parse; pub mod option_parse; +pub mod relation_parse; +pub mod summary_parse; +pub mod time_parse; +pub mod translate_parse; pub mod util; diff --git a/collab-database/src/template/relation_parse.rs b/collab-database/src/template/relation_parse.rs new file mode 100644 index 000000000..5c2c798d2 --- /dev/null +++ b/collab-database/src/template/relation_parse.rs @@ -0,0 +1,61 @@ +use crate::entity::FieldType; +use crate::rows::{new_cell_builder, Cell, RowId}; +use crate::template::entity::CELL_DATA; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use yrs::Any; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RelationCellData { + pub row_ids: Vec, +} + +impl From<&Cell> for RelationCellData { + fn from(value: &Cell) -> Self { + let row_ids = match value.get(CELL_DATA) { + Some(Any::Array(array)) => array + .iter() + .flat_map(|item| { + if let Any::String(string) = item { + Some(RowId::from(string.clone().to_string())) + } else { + None + } + }) + .collect(), + _ => vec![], + }; + Self { row_ids } + } +} + +impl From<&RelationCellData> for Cell { + fn from(value: &RelationCellData) -> Self { + let data = Any::Array(Arc::from( + value + .row_ids + .clone() + .into_iter() + .map(|id| Any::String(Arc::from(id.to_string()))) + .collect::>(), + )); + let mut cell = new_cell_builder(FieldType::Relation); + cell.insert(CELL_DATA.into(), data); + cell + } +} + +impl From<&str> for RelationCellData { + fn from(s: &str) -> Self { + if s.is_empty() { + return RelationCellData { row_ids: vec![] }; + } + + let ids = s + .split(", ") + .map(|id| id.to_string().into()) + .collect::>(); + + RelationCellData { row_ids: ids } + } +} diff --git a/collab-database/src/template/summary_parse.rs b/collab-database/src/template/summary_parse.rs new file mode 100644 index 000000000..ddf18097a --- /dev/null +++ b/collab-database/src/template/summary_parse.rs @@ -0,0 +1,41 @@ +use crate::entity::FieldType; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct SummaryCellData(pub String); +impl std::ops::Deref for SummaryCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&Cell> for SummaryCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_as::(CELL_DATA).unwrap_or_default()) + } +} + +impl From for Cell { + fn from(data: SummaryCellData) -> Self { + let mut cell = new_cell_builder(FieldType::Summary); + cell.insert(CELL_DATA.into(), data.0.into()); + cell + } +} + +impl ToString for SummaryCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef for SummaryCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/collab-database/src/template/time_parse.rs b/collab-database/src/template/time_parse.rs new file mode 100644 index 000000000..77453bd18 --- /dev/null +++ b/collab-database/src/template/time_parse.rs @@ -0,0 +1,42 @@ +use crate::entity::FieldType; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct TimeCellData(pub Option); + +impl From<&Cell> for TimeCellData { + fn from(cell: &Cell) -> Self { + Self( + cell + .get_as::(CELL_DATA) + .and_then(|data| data.parse::().ok()), + ) + } +} + +impl std::convert::From<&str> for TimeCellData { + fn from(s: &str) -> Self { + Self(s.trim().to_string().parse::().ok()) + } +} + +impl ToString for TimeCellData { + fn to_string(&self) -> String { + if let Some(time) = self.0 { + time.to_string() + } else { + "".to_string() + } + } +} + +impl From<&TimeCellData> for Cell { + fn from(data: &TimeCellData) -> Self { + let mut cell = new_cell_builder(FieldType::Time); + cell.insert(CELL_DATA.into(), data.to_string().into()); + cell + } +} diff --git a/collab-database/src/template/translate_parse.rs b/collab-database/src/template/translate_parse.rs new file mode 100644 index 000000000..34c4a8a8d --- /dev/null +++ b/collab-database/src/template/translate_parse.rs @@ -0,0 +1,41 @@ +use crate::entity::FieldType; +use crate::rows::{new_cell_builder, Cell}; +use crate::template::entity::CELL_DATA; +use collab::util::AnyMapExt; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct TranslateCellData(pub String); +impl std::ops::Deref for TranslateCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&Cell> for TranslateCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_as(CELL_DATA).unwrap_or_default()) + } +} + +impl From for Cell { + fn from(data: TranslateCellData) -> Self { + let mut cell = new_cell_builder(FieldType::Translate); + cell.insert(CELL_DATA.into(), data.0.into()); + cell + } +} + +impl ToString for TranslateCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef for TranslateCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/collab-database/tests/database_test/cell_type_option_test.rs b/collab-database/tests/database_test/cell_type_option_test.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/collab-database/tests/database_test/cell_type_option_test.rs @@ -0,0 +1 @@ + diff --git a/collab-database/tests/database_test/mod.rs b/collab-database/tests/database_test/mod.rs index d6152c11b..fd55e2147 100644 --- a/collab-database/tests/database_test/mod.rs +++ b/collab-database/tests/database_test/mod.rs @@ -1,5 +1,6 @@ mod block_test; mod cell_test; +mod cell_type_option_test; mod encode_collab_test; mod field_observe_test; mod field_setting_test; diff --git a/collab-importer/tests/notion_test/import_test.rs b/collab-importer/tests/notion_test/import_test.rs index 2d3441608..add7351e4 100644 --- a/collab-importer/tests/notion_test/import_test.rs +++ b/collab-importer/tests/notion_test/import_test.rs @@ -5,7 +5,7 @@ use collab_database::entity::FieldType; use collab_database::entity::FieldType::*; use collab_database::error::DatabaseError; use collab_database::fields::media_type_option::MediaCellData; -use collab_database::fields::{Field, StringifyTypeOption}; +use collab_database::fields::{Field, TypeOptionCellReader}; use collab_database::rows::Row; use collab_document::blocks::{ extract_page_id_from_block_delta, extract_view_id_from_block_data, @@ -675,7 +675,7 @@ fn assert_database_rows_with_csv_rows( .map(|field| { ( field.id.clone(), - match database.get_stringify_type_option(&field.id) { + match database.get_cell_reader(&field.id) { None => { panic!("Field {:?} doesn't have type option", field) }, @@ -683,7 +683,7 @@ fn assert_database_rows_with_csv_rows( }, ) }) - .collect::>>(); + .collect::>>(); for (row_index, row) in rows.into_iter().enumerate() { let row = row.unwrap();