Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions collab-database/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Box<dyn StringifyTypeOption>> {
/// Return [TypeOptionCellReader] for the given field id.
pub fn get_cell_reader(&self, field_id: &str) -> Option<Box<dyn TypeOptionCellReader>> {
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<Box<dyn TypeOptionCellWriter>> {
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)]
Expand Down
136 changes: 133 additions & 3 deletions collab-database/src/fields/type_option/checkbox_type_option.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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::<String>(CELL_DATA) {
None => "".to_string(),
Some(s) => Self::convert_raw_cell_data(self, &s),
};
Value::String(value)
}

fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let cell_data = cell.get_as::<String>(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<CheckboxTypeOption> for TypeOptionData {
fn from(_data: CheckboxTypeOption) -> Self {
TypeOptionDataBuilder::new()
Expand All @@ -27,3 +66,94 @@ impl From<TypeOptionData> 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::<String>(CELL_DATA).unwrap(), "true");

// Write a boolean
let value = Value::Bool(true);
let cell = option.write_json(value);
assert_eq!(cell.get_as::<String>(CELL_DATA).unwrap(), "true");

// Write a number
let value = Value::Number(1.into());
let cell = option.write_json(value);
assert_eq!(cell.get_as::<String>(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(""));
}
}
127 changes: 125 additions & 2 deletions collab-database/src/fields/type_option/checklist_type_option.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,3 +23,119 @@ impl From<ChecklistTypeOption> 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<f64> {
None
}

fn convert_raw_cell_data(&self, cell_data: &str) -> String {
let cell_data = serde_json::from_str::<ChecklistCellData>(cell_data).unwrap_or_default();
cell_data
.options
.into_iter()
.map(|option| option.name)
.collect::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR)
}
}

impl TypeOptionCellWriter for ChecklistTypeOption {
fn write_json(&self, json_value: Value) -> Cell {
let cell_data = serde_json::from_value::<ChecklistCellData>(json_value).unwrap_or_default();
cell_data.into()
}
}

impl From<&Cell> for ChecklistCellData {
fn from(cell: &Cell) -> Self {
cell
.get_as::<String>(CELL_DATA)
.map(|data| serde_json::from_str::<ChecklistCellData>(&data).unwrap_or_default())
.unwrap_or_default()
}
}

impl From<ChecklistCellData> 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);
}
}
Loading
Loading