diff --git a/collab/src/database/entity.rs b/collab/src/database/entity.rs index b205e7eed..a7fc35cdd 100644 --- a/collab/src/database/entity.rs +++ b/collab/src/database/entity.rs @@ -9,6 +9,7 @@ use crate::database::fields::media_type_option::MediaTypeOption; use crate::database::fields::number_type_option::NumberTypeOption; use crate::database::fields::person_type_option::PersonTypeOption; use crate::database::fields::relation_type_option::RelationTypeOption; +use crate::database::fields::rollup_type_option::RollupTypeOption; use crate::database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; use crate::database::fields::summary_type_option::SummarizationTypeOption; use crate::database::fields::text_type_option::RichTextTypeOption; @@ -307,6 +308,7 @@ pub enum FieldType { Time = 13, Media = 14, Person = 15, + Rollup = 16, } impl FieldType { @@ -380,6 +382,7 @@ impl FieldType { FieldType::Time => "Time", FieldType::Media => "Media", FieldType::Person => "Person", + FieldType::Rollup => "Rollup", }; s.to_string() } @@ -436,6 +439,10 @@ impl FieldType { matches!(self, FieldType::Relation) } + pub fn is_rollup(&self) -> bool { + matches!(self, FieldType::Rollup) + } + pub fn is_time(&self) -> bool { matches!(self, FieldType::Time) } @@ -472,6 +479,7 @@ impl From for FieldType { 13 => FieldType::Time, 14 => FieldType::Media, 15 => FieldType::Person, + 16 => FieldType::Rollup, _ => { error!("Unknown field type: {}, fallback to text", index); FieldType::RichText @@ -501,6 +509,7 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa FieldType::Summary => SummarizationTypeOption::default().into(), FieldType::Translate => TranslateTypeOption::default().into(), FieldType::Person => PersonTypeOption::default().into(), + FieldType::Rollup => RollupTypeOption::default().into(), } } diff --git a/collab/src/database/fields/type_option/mod.rs b/collab/src/database/fields/type_option/mod.rs index 3d98ee68d..bbb3966a4 100644 --- a/collab/src/database/fields/type_option/mod.rs +++ b/collab/src/database/fields/type_option/mod.rs @@ -5,6 +5,7 @@ pub mod media_type_option; pub mod number_type_option; pub mod person_type_option; pub mod relation_type_option; +pub mod rollup_type_option; pub mod select_type_option; pub mod summary_type_option; pub mod text_type_option; @@ -22,6 +23,7 @@ use crate::database::fields::media_type_option::MediaTypeOption; use crate::database::fields::number_type_option::NumberTypeOption; use crate::database::fields::person_type_option::PersonTypeOption; use crate::database::fields::relation_type_option::RelationTypeOption; +use crate::database::fields::rollup_type_option::RollupTypeOption; use crate::database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; use crate::database::fields::summary_type_option::SummarizationTypeOption; use crate::database::fields::timestamp_type_option::TimestampTypeOption; @@ -181,6 +183,7 @@ pub fn type_option_cell_writer( FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)), FieldType::Translate => Box::new(TranslateTypeOption::from(type_option_data)), FieldType::Person => Box::new(PersonTypeOption::from(type_option_data)), + FieldType::Rollup => Box::new(RollupTypeOption::from(type_option_data)), } } @@ -205,5 +208,6 @@ pub fn type_option_cell_reader( FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)), FieldType::Translate => Box::new(TranslateTypeOption::from(type_option_data)), FieldType::Person => Box::new(PersonTypeOption::from(type_option_data)), + FieldType::Rollup => Box::new(RollupTypeOption::from(type_option_data)), } } diff --git a/collab/src/database/fields/type_option/rollup_type_option.rs b/collab/src/database/fields/type_option/rollup_type_option.rs new file mode 100644 index 000000000..e837085aa --- /dev/null +++ b/collab/src/database/fields/type_option/rollup_type_option.rs @@ -0,0 +1,129 @@ +use super::{TypeOptionData, TypeOptionDataBuilder}; +use crate::database::entity::FieldType; +use crate::database::fields::{TypeOptionCellReader, TypeOptionCellWriter}; +use crate::database::rows::{Cell, new_cell_builder}; +use crate::database::template::entity::CELL_DATA; +use crate::util::AnyMapExt; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use yrs::Any; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize_repr, Deserialize_repr)] +#[repr(i64)] +pub enum RollupDisplayMode { + #[default] + Calculated = 0, + OriginalList = 1, + UniqueList = 2, +} + +impl From for RollupDisplayMode { + fn from(value: i64) -> Self { + match value { + 0 => RollupDisplayMode::Calculated, + 1 => RollupDisplayMode::OriginalList, + 2 => RollupDisplayMode::UniqueList, + _ => RollupDisplayMode::Calculated, + } + } +} + +impl From for i64 { + fn from(value: RollupDisplayMode) -> Self { + value as i64 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RollupTypeOption { + pub relation_field_id: String, + pub target_field_id: String, + pub calculation_type: i64, + pub show_as: RollupDisplayMode, + #[serde(default)] + pub condition_value: String, +} + +impl Default for RollupTypeOption { + fn default() -> Self { + Self { + relation_field_id: String::new(), + target_field_id: String::new(), + // Default to Count, which is applicable across all field types. + calculation_type: 5, + show_as: RollupDisplayMode::Calculated, + condition_value: String::new(), + } + } +} + +impl From for RollupTypeOption { + fn from(data: TypeOptionData) -> Self { + let relation_field_id: String = data.get_as("relation_field_id").unwrap_or_default(); + let target_field_id: String = data.get_as("target_field_id").unwrap_or_default(); + let calculation_type: i64 = data.get_as("calculation_type").unwrap_or(5); + let show_as: i64 = data.get_as("show_as").unwrap_or(0); + let condition_value: String = data.get_as("condition_value").unwrap_or_default(); + Self { + relation_field_id, + target_field_id, + calculation_type, + show_as: show_as.into(), + condition_value, + } + } +} + +impl From for TypeOptionData { + fn from(data: RollupTypeOption) -> Self { + TypeOptionDataBuilder::from([ + ( + "relation_field_id".into(), + Any::String(data.relation_field_id.into()), + ), + ( + "target_field_id".into(), + Any::String(data.target_field_id.into()), + ), + ( + "calculation_type".into(), + Any::BigInt(data.calculation_type), + ), + ("show_as".into(), Any::BigInt(i64::from(data.show_as))), + ( + "condition_value".into(), + Any::String(data.condition_value.into()), + ), + ]) + } +} + +impl TypeOptionCellReader for RollupTypeOption { + 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, cell_data: &str) -> String { + cell_data.to_string() + } +} + +impl TypeOptionCellWriter for RollupTypeOption { + fn convert_json_to_cell(&self, json_value: Value) -> Cell { + let mut cell = new_cell_builder(FieldType::Rollup); + match json_value { + Value::String(value_str) => { + cell.insert(CELL_DATA.into(), value_str.into()); + }, + _ => { + cell.insert(CELL_DATA.into(), json_value.to_string().into()); + }, + } + cell + } +} diff --git a/collab/src/entity/proto/collab.rs b/collab/src/entity/proto/collab.rs index e055e5569..d1f05ef67 100644 --- a/collab/src/entity/proto/collab.rs +++ b/collab/src/entity/proto/collab.rs @@ -1,4 +1,3 @@ -// This file is @generated by prost-build. /// Originating from an AppFlowy Client. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/collab/tests/database/database_test/mod.rs b/collab/tests/database/database_test/mod.rs index 323a65c06..b333ad35b 100644 --- a/collab/tests/database/database_test/mod.rs +++ b/collab/tests/database/database_test/mod.rs @@ -10,6 +10,7 @@ mod group_test; pub mod helper; mod layout_test; // mod restore_test; +mod rollup_type_option_test; mod row_observe_test; mod row_test; mod sort_test; diff --git a/collab/tests/database/database_test/rollup_type_option_test.rs b/collab/tests/database/database_test/rollup_type_option_test.rs new file mode 100644 index 000000000..4d6714835 --- /dev/null +++ b/collab/tests/database/database_test/rollup_type_option_test.rs @@ -0,0 +1,279 @@ +use collab::database::entity::{FieldType, default_type_option_data_from_type}; +use collab::database::fields::rollup_type_option::{RollupDisplayMode, RollupTypeOption}; +use collab::database::fields::{TypeOptionCellReader, TypeOptionCellWriter, TypeOptionData}; +use collab::database::rows::Cell; +use collab::database::template::entity::CELL_DATA; +use collab::util::AnyMapExt; +use serde_json::json; +use yrs::Any; + +// ==================== RollupDisplayMode Tests ==================== + +#[test] +fn rollup_display_mode_from_i64_calculated() { + assert_eq!(RollupDisplayMode::from(0), RollupDisplayMode::Calculated); +} + +#[test] +fn rollup_display_mode_from_i64_original_list() { + assert_eq!(RollupDisplayMode::from(1), RollupDisplayMode::OriginalList); +} + +#[test] +fn rollup_display_mode_from_i64_unique_list() { + assert_eq!(RollupDisplayMode::from(2), RollupDisplayMode::UniqueList); +} + +#[test] +fn rollup_display_mode_from_i64_unknown_defaults_to_calculated() { + assert_eq!(RollupDisplayMode::from(99), RollupDisplayMode::Calculated); + assert_eq!(RollupDisplayMode::from(-1), RollupDisplayMode::Calculated); +} + +#[test] +fn rollup_display_mode_to_i64() { + assert_eq!(i64::from(RollupDisplayMode::Calculated), 0); + assert_eq!(i64::from(RollupDisplayMode::OriginalList), 1); + assert_eq!(i64::from(RollupDisplayMode::UniqueList), 2); +} + +#[test] +fn rollup_display_mode_default() { + assert_eq!(RollupDisplayMode::default(), RollupDisplayMode::Calculated); +} + +// ==================== RollupTypeOption Default Tests ==================== + +#[test] +fn rollup_type_option_default_values() { + let option = RollupTypeOption::default(); + assert_eq!(option.relation_field_id, ""); + assert_eq!(option.target_field_id, ""); + assert_eq!(option.calculation_type, 5); // Default to Count + assert_eq!(option.show_as, RollupDisplayMode::Calculated); + assert_eq!(option.condition_value, ""); +} + +// ==================== RollupTypeOption Serialization Tests ==================== + +#[test] +fn rollup_type_option_to_type_option_data() { + let option = RollupTypeOption { + relation_field_id: "rel_field_1".to_string(), + target_field_id: "target_field_1".to_string(), + calculation_type: 3, + show_as: RollupDisplayMode::OriginalList, + condition_value: "some_condition".to_string(), + }; + + let data: TypeOptionData = option.into(); + + assert_eq!( + data.get_as::("relation_field_id").unwrap(), + "rel_field_1" + ); + assert_eq!( + data.get_as::("target_field_id").unwrap(), + "target_field_1" + ); + assert_eq!(data.get_as::("calculation_type").unwrap(), 3); + assert_eq!(data.get_as::("show_as").unwrap(), 1); + assert_eq!( + data.get_as::("condition_value").unwrap(), + "some_condition" + ); +} + +#[test] +fn rollup_type_option_from_type_option_data() { + let mut data = TypeOptionData::new(); + data.insert( + "relation_field_id".to_string(), + Any::String("rel_field_2".into()), + ); + data.insert( + "target_field_id".to_string(), + Any::String("target_field_2".into()), + ); + data.insert("calculation_type".to_string(), Any::BigInt(7)); + data.insert("show_as".to_string(), Any::BigInt(2)); + data.insert( + "condition_value".to_string(), + Any::String("condition_2".into()), + ); + + let option = RollupTypeOption::from(data); + + assert_eq!(option.relation_field_id, "rel_field_2"); + assert_eq!(option.target_field_id, "target_field_2"); + assert_eq!(option.calculation_type, 7); + assert_eq!(option.show_as, RollupDisplayMode::UniqueList); + assert_eq!(option.condition_value, "condition_2"); +} + +#[test] +fn rollup_type_option_from_empty_type_option_data() { + let data = TypeOptionData::new(); + let option = RollupTypeOption::from(data); + + // Should use default values when data is missing + assert_eq!(option.relation_field_id, ""); + assert_eq!(option.target_field_id, ""); + assert_eq!(option.calculation_type, 5); // Default to Count + assert_eq!(option.show_as, RollupDisplayMode::Calculated); + assert_eq!(option.condition_value, ""); +} + +#[test] +fn rollup_type_option_roundtrip() { + let original = RollupTypeOption { + relation_field_id: "rel_123".to_string(), + target_field_id: "target_456".to_string(), + calculation_type: 10, + show_as: RollupDisplayMode::UniqueList, + condition_value: "my_condition".to_string(), + }; + + let data: TypeOptionData = original.clone().into(); + let restored = RollupTypeOption::from(data); + + assert_eq!(restored.relation_field_id, original.relation_field_id); + assert_eq!(restored.target_field_id, original.target_field_id); + assert_eq!(restored.calculation_type, original.calculation_type); + assert_eq!(restored.show_as, original.show_as); + assert_eq!(restored.condition_value, original.condition_value); +} + +// ==================== TypeOptionCellReader Tests ==================== + +#[test] +fn rollup_type_option_json_cell() { + let option = RollupTypeOption::default(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.to_string(), Any::String("test_value".into())); + + let json = option.json_cell(&cell); + assert_eq!(json, json!("test_value")); +} + +#[test] +fn rollup_type_option_json_cell_empty() { + let option = RollupTypeOption::default(); + let cell = Cell::new(); + + let json = option.json_cell(&cell); + assert_eq!(json, json!("")); +} + +#[test] +fn rollup_type_option_numeric_cell_valid() { + let option = RollupTypeOption::default(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.to_string(), Any::String("42.5".into())); + + let numeric = option.numeric_cell(&cell); + assert_eq!(numeric, Some(42.5)); +} + +#[test] +fn rollup_type_option_numeric_cell_invalid() { + let option = RollupTypeOption::default(); + let mut cell = Cell::new(); + cell.insert(CELL_DATA.to_string(), Any::String("not_a_number".into())); + + let numeric = option.numeric_cell(&cell); + assert_eq!(numeric, None); +} + +#[test] +fn rollup_type_option_convert_raw_cell_data() { + let option = RollupTypeOption::default(); + let result = option.convert_raw_cell_data("raw_data_test"); + assert_eq!(result, "raw_data_test"); +} + +// ==================== TypeOptionCellWriter Tests ==================== + +#[test] +fn rollup_type_option_convert_json_string_to_cell() { + let option = RollupTypeOption::default(); + let json_value = json!("hello world"); + + let cell = option.convert_json_to_cell(json_value); + + assert_eq!( + cell.get(CELL_DATA).unwrap(), + &Any::String("hello world".into()) + ); +} + +#[test] +fn rollup_type_option_convert_json_number_to_cell() { + let option = RollupTypeOption::default(); + let json_value = json!(123); + + let cell = option.convert_json_to_cell(json_value); + + assert_eq!(cell.get(CELL_DATA).unwrap(), &Any::String("123".into())); +} + +#[test] +fn rollup_type_option_convert_json_object_to_cell() { + let option = RollupTypeOption::default(); + let json_value = json!({"key": "value"}); + + let cell = option.convert_json_to_cell(json_value); + + assert_eq!( + cell.get(CELL_DATA).unwrap(), + &Any::String("{\"key\":\"value\"}".into()) + ); +} + +#[test] +fn rollup_type_option_convert_json_array_to_cell() { + let option = RollupTypeOption::default(); + let json_value = json!([1, 2, 3]); + + let cell = option.convert_json_to_cell(json_value); + + assert_eq!(cell.get(CELL_DATA).unwrap(), &Any::String("[1,2,3]".into())); +} + +// ==================== FieldType::Rollup Tests ==================== + +#[test] +fn field_type_rollup_value() { + assert_eq!(FieldType::Rollup as i64, 16); +} + +#[test] +fn field_type_from_i64_rollup() { + let field_type = FieldType::from(16_i64); + assert_eq!(field_type, FieldType::Rollup); +} + +#[test] +fn field_type_rollup_is_rollup() { + assert!(FieldType::Rollup.is_rollup()); + assert!(!FieldType::RichText.is_rollup()); + assert!(!FieldType::Relation.is_rollup()); +} + +#[test] +fn field_type_rollup_default_name() { + assert_eq!(FieldType::Rollup.default_name(), "Rollup"); +} + +#[test] +fn field_type_rollup_default_type_option_data() { + let data = default_type_option_data_from_type(FieldType::Rollup); + let option = RollupTypeOption::from(data); + + // Verify it creates a valid default RollupTypeOption + assert_eq!(option.relation_field_id, ""); + assert_eq!(option.target_field_id, ""); + assert_eq!(option.calculation_type, 5); + assert_eq!(option.show_as, RollupDisplayMode::Calculated); + assert_eq!(option.condition_value, ""); +}