diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs index 4acb3a9941..97597f2d9b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs @@ -9,12 +9,11 @@ pub struct ChecklistFilterPB { pub condition: ChecklistFilterConditionPB, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] -#[derive(Default)] pub enum ChecklistFilterConditionPB { - IsComplete = 0, #[default] + IsComplete = 0, IsIncomplete = 1, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs index 202f7a316b..1a186eb038 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs @@ -1,6 +1,7 @@ +use collab_database::{fields::Field, rows::Cell}; use flowy_derive::ProtoBuf; -use crate::services::filter::ParseFilterData; +use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RelationFilterPB { @@ -13,3 +14,9 @@ impl ParseFilterData for RelationFilterPB { RelationFilterPB { condition: 0 } } } + +impl PreFillCellsWithFilter for RelationFilterPB { + fn get_compliant_cell(&self, _field: &Field) -> (Option, bool) { + (None, false) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index dd3ee55f26..2208bdeb23 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -148,7 +148,8 @@ impl DatabaseViewEditor { } // fill in cells according to active filters - // TODO(RS) + let filter_controller = self.filter_controller.clone(); + filter_controller.fill_cells(&mut cells).await; result.collab_params.cells = cells; @@ -739,6 +740,12 @@ impl DatabaseViewEditor { } pub async fn v_did_delete_field(&self, deleted_field_id: &str) { + let changeset = FilterChangeset::DeleteAllWithFieldId { + field_id: deleted_field_id.to_string(), + }; + let notification = self.filter_controller.apply_changeset(changeset).await; + notify_did_update_filter(notification).await; + let sorts = self.delegate.get_all_sorts(&self.view_id); if let Some(sort) = sorts.iter().find(|sort| sort.field_id == deleted_field_id) { @@ -801,11 +808,10 @@ impl DatabaseViewEditor { .await; if old_field.field_type != field.field_type { - let filter_controller = self.filter_controller.clone(); let changeset = FilterChangeset::DeleteAllWithFieldId { field_id: field.id.clone(), }; - let notification = filter_controller.apply_changeset(changeset).await; + let notification = self.filter_controller.apply_changeset(changeset).await; notify_did_update_filter(notification).await; } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 8e7daa472c..f710144e60 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -17,7 +17,6 @@ pub async fn make_filter_controller( notifier: DatabaseViewChangedNotifier, cell_cache: CellCache, ) -> Arc { - let filters = delegate.get_all_filters(view_id); let task_scheduler = delegate.get_task_scheduler(); let filter_delegate = DatabaseViewFilterDelegateImpl(delegate.clone()); @@ -27,7 +26,6 @@ pub async fn make_filter_controller( &handler_id, filter_delegate, task_scheduler.clone(), - filters, cell_cache, notifier, ) @@ -62,6 +60,10 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl { self.0.get_row(view_id, rows_id) } + fn get_all_filters(&self, view_id: &str) -> Vec { + self.0.get_all_filters(view_id) + } + fn save_filters(&self, view_id: &str, filters: &[Filter]) { self.0.save_filters(view_id, filters) } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs index 9a1e2812e1..e2aa56de94 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs @@ -1,4 +1,8 @@ +use collab_database::{fields::Field, rows::Cell}; + use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB}; +use crate::services::cell::insert_checkbox_cell; +use crate::services::filter::PreFillCellsWithFilter; impl CheckboxFilterPB { pub fn is_visible(&self, cell_data: &CheckboxCellDataPB) -> bool { @@ -9,6 +13,20 @@ impl CheckboxFilterPB { } } +impl PreFillCellsWithFilter for CheckboxFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let is_checked = match self.condition { + CheckboxFilterConditionPB::IsChecked => Some(true), + CheckboxFilterConditionPB::IsUnChecked => None, + }; + + ( + is_checked.map(|is_checked| insert_checkbox_cell(is_checked, field)), + false, + ) + } +} + #[cfg(test)] mod tests { use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB}; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs index 84773b5cd3..91768a5cf3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs @@ -1,5 +1,9 @@ +use collab_database::fields::Field; +use collab_database::rows::Cell; + use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB}; use crate::services::field::SelectOption; +use crate::services::filter::PreFillCellsWithFilter; impl ChecklistFilterPB { pub fn is_visible( @@ -37,3 +41,9 @@ impl ChecklistFilterPB { } } } + +impl PreFillCellsWithFilter for ChecklistFilterPB { + fn get_compliant_cell(&self, _field: &Field) -> (Option, bool) { + (None, true) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs index 1eed418c86..42a0300e18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs @@ -1,8 +1,11 @@ use crate::entities::{DateFilterConditionPB, DateFilterPB}; +use crate::services::cell::insert_date_cell; +use crate::services::field::DateCellData; +use crate::services::filter::PreFillCellsWithFilter; -use chrono::{NaiveDate, NaiveDateTime}; - -use super::DateCellData; +use chrono::{Duration, NaiveDate, NaiveDateTime}; +use collab_database::fields::Field; +use collab_database::rows::Cell; impl DateFilterPB { /// Returns `None` if the DateFilterPB doesn't have the necessary data for @@ -95,6 +98,39 @@ impl DateFilterStrategy { } } +impl PreFillCellsWithFilter for DateFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let timestamp = match self.condition { + DateFilterConditionPB::DateIs + | DateFilterConditionPB::DateOnOrBefore + | DateFilterConditionPB::DateOnOrAfter => self.timestamp, + DateFilterConditionPB::DateBefore => self + .timestamp + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { + let answer = date_time - Duration::days(1); + answer.timestamp() + }), + DateFilterConditionPB::DateAfter => self + .timestamp + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { + let answer = date_time + Duration::days(1); + answer.timestamp() + }), + DateFilterConditionPB::DateWithIn => self.start, + _ => None, + }; + + let open_after_create = matches!(self.condition, DateFilterConditionPB::DateIsNotEmpty); + + ( + timestamp.map(|timestamp| insert_date_cell(timestamp, None, None, field)), + open_after_create, + ) + } +} + #[cfg(test)] mod tests { use crate::entities::{DateFilterConditionPB, DateFilterPB}; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs index 3026964d6c..ba95dd8843 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs @@ -1,9 +1,13 @@ use std::str::FromStr; +use collab_database::fields::Field; +use collab_database::rows::Cell; use rust_decimal::Decimal; use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; +use crate::services::cell::insert_text_cell; use crate::services::field::NumberCellFormat; +use crate::services::filter::PreFillCellsWithFilter; impl NumberFilterPB { pub fn is_visible(&self, cell_data: &NumberCellFormat) -> Option { @@ -30,6 +34,39 @@ impl NumberFilterPB { } } +impl PreFillCellsWithFilter for NumberFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let expected_decimal = || Decimal::from_str(&self.content).ok(); + + let text = match self.condition { + NumberFilterConditionPB::Equal + | NumberFilterConditionPB::GreaterThanOrEqualTo + | NumberFilterConditionPB::LessThanOrEqualTo + if !self.content.is_empty() => + { + Some(self.content.clone()) + }, + NumberFilterConditionPB::GreaterThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value + Decimal::from_f32_retain(1.0).unwrap(); + answer.to_string() + }) + }, + NumberFilterConditionPB::LessThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value - Decimal::from_f32_retain(1.0).unwrap(); + answer.to_string() + }) + }, + _ => None, + }; + + let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty); + + // use `insert_text_cell` because self.content might not be a parsable i64. + (text.map(|s| insert_text_cell(s, field)), open_after_create) + } +} enum NumberFilterStrategy { Equal(Decimal), NotEqual(Decimal), diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs index a1ff7e198a..a0e1ce096b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs @@ -1,5 +1,10 @@ +use collab_database::fields::Field; +use collab_database::rows::Cell; + use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; -use crate::services::field::SelectOption; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{select_type_option_from_field, SelectOption}; +use crate::services::filter::PreFillCellsWithFilter; impl SelectOptionFilterPB { pub fn is_visible(&self, selected_options: &[SelectOption]) -> Option { @@ -90,6 +95,40 @@ impl SelectOptionFilterStrategy { } } +impl PreFillCellsWithFilter for SelectOptionFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let get_non_empty_expected_options = || { + if !self.option_ids.is_empty() { + Some(self.option_ids.clone()) + } else { + None + } + }; + + let option_ids = match self.condition { + SelectOptionFilterConditionPB::OptionIs => get_non_empty_expected_options(), + SelectOptionFilterConditionPB::OptionContains => { + get_non_empty_expected_options().map(|mut options| vec![options.swap_remove(0)]) + }, + SelectOptionFilterConditionPB::OptionIsNotEmpty => select_type_option_from_field(field) + .ok() + .map(|mut type_option| { + let options = type_option.mut_options(); + if options.is_empty() { + vec![] + } else { + vec![options.swap_remove(0).id] + } + }), + _ => None, + }; + + ( + option_ids.map(|ids| insert_select_option_cell(ids, field)), + false, + ) + } +} #[cfg(test)] mod tests { use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs index 0d966da381..8f090f5802 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs @@ -1,4 +1,8 @@ +use collab_database::{fields::Field, rows::Cell}; + use crate::entities::{TextFilterConditionPB, TextFilterPB}; +use crate::services::cell::insert_text_cell; +use crate::services::filter::PreFillCellsWithFilter; impl TextFilterPB { pub fn is_visible>(&self, cell_data: T) -> bool { @@ -17,6 +21,26 @@ impl TextFilterPB { } } +impl PreFillCellsWithFilter for TextFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let text = match self.condition { + TextFilterConditionPB::TextIs + | TextFilterConditionPB::TextContains + | TextFilterConditionPB::TextStartsWith + | TextFilterConditionPB::TextEndsWith + if !self.content.is_empty() => + { + Some(self.content.clone()) + }, + _ => None, + }; + + let open_after_create = matches!(self.condition, TextFilterConditionPB::TextIsNotEmpty); + + (text.map(|s| insert_text_cell(s, field)), open_after_create) + } +} + #[cfg(test)] mod tests { #![allow(clippy::all)] diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index 905919015b..281ed3e684 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -19,7 +19,7 @@ use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption, }; -use crate::services::filter::ParseFilterData; +use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; use crate::services::sort::SortCondition; pub trait TypeOption { @@ -58,7 +58,7 @@ pub trait TypeOption { type CellProtobufType: TryInto + Debug; /// Represents the filter configuration for this type option. - type CellFilter: ParseFilterData + Clone + Send + Sync + 'static; + type CellFilter: ParseFilterData + PreFillCellsWithFilter + Clone + Send + Sync + 'static; } /// This trait providing serialization and deserialization methods for cell data. /// diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 2323a3ce02..6f700ca7c0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use collab_database::database::gen_database_filter_id; use collab_database::fields::Field; -use collab_database::rows::{Row, RowDetail, RowId}; +use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -25,9 +25,14 @@ pub trait FilterDelegate: Send + Sync + 'static { fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; fn get_rows(&self, view_id: &str) -> Fut>>; fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; + fn get_all_filters(&self, view_id: &str) -> Vec; fn save_filters(&self, view_id: &str, filters: &[Filter]); } +pub trait PreFillCellsWithFilter { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool); +} + pub struct FilterController { view_id: String, handler_id: String, @@ -51,13 +56,46 @@ impl FilterController { handler_id: &str, delegate: T, task_scheduler: Arc>, - filters: Vec, cell_cache: CellCache, notifier: DatabaseViewChangedNotifier, ) -> Self where T: FilterDelegate + 'static, { + // ensure every filter is valid + let field_ids = delegate + .get_fields(view_id, None) + .await + .into_iter() + .map(|field| field.id) + .collect::>(); + + let mut need_save = false; + + let mut filters = delegate.get_all_filters(view_id); + let mut filtering_field_ids: HashMap> = HashMap::new(); + + for filter in filters.iter() { + filter.get_all_filtering_field_ids(&mut filtering_field_ids); + } + + let mut delete_filter_ids = vec![]; + + for (field_id, filter_ids) in &filtering_field_ids { + if !field_ids.contains(field_id) { + need_save = true; + delete_filter_ids.extend(filter_ids); + } + } + + for filter_id in delete_filter_ids { + Self::delete_filter(&mut filters, filter_id); + } + + if need_save { + delegate.save_filters(view_id, &filters); + } + Self { view_id: view_id.to_string(), handler_id: handler_id.to_string(), @@ -116,106 +154,6 @@ impl FilterController { }); } - async fn get_field_map(&self) -> HashMap { - self - .delegate - .get_fields(&self.view_id, None) - .await - .into_iter() - .map(|field| (field.id.clone(), field)) - .collect::>() - } - - #[tracing::instrument( - name = "process_filter_task", - level = "trace", - skip_all, - fields(filter_result), - err - )] - pub async fn process(&self, predicate: &str) -> FlowyResult<()> { - let event_type = FilterEvent::from_str(predicate).unwrap(); - match event_type { - FilterEvent::FilterDidChanged => self.filter_all_rows().await?, - FilterEvent::RowDidChanged(row_id) => self.filter_single_row(row_id).await?, - } - Ok(()) - } - - async fn filter_single_row(&self, row_id: RowId) -> FlowyResult<()> { - let filters = self.filters.read().await; - - if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await { - let field_by_field_id = self.get_field_map().await; - let mut notification = FilterResultNotification::new(self.view_id.clone()); - if let Some((row_id, is_visible)) = filter_row( - &row_detail.row, - &self.result_by_row_id, - &field_by_field_id, - &self.cell_cache, - &filters, - ) { - if is_visible { - if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { - notification.visible_rows.push( - InsertedRowPB::new(RowMetaPB::from(row_detail.as_ref())).with_index(index as i32), - ) - } - } else { - notification.invisible_rows.push(row_id); - } - } - - let _ = self - .notifier - .send(DatabaseViewChanged::FilterNotification(notification)); - } - Ok(()) - } - - async fn filter_all_rows(&self) -> FlowyResult<()> { - let filters = self.filters.read().await; - - let field_by_field_id = self.get_field_map().await; - let mut visible_rows = vec![]; - let mut invisible_rows = vec![]; - - for (index, row_detail) in self - .delegate - .get_rows(&self.view_id) - .await - .into_iter() - .enumerate() - { - if let Some((row_id, is_visible)) = filter_row( - &row_detail.row, - &self.result_by_row_id, - &field_by_field_id, - &self.cell_cache, - &filters, - ) { - if is_visible { - let row_meta = RowMetaPB::from(row_detail.as_ref()); - visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) - } else { - invisible_rows.push(row_id); - } - } - } - - let notification = FilterResultNotification { - view_id: self.view_id.clone(), - invisible_rows, - visible_rows, - }; - tracing::trace!("filter result {:?}", filters); - let _ = self - .notifier - .send(DatabaseViewChanged::FilterNotification(notification)); - - Ok(()) - } - pub async fn did_receive_row_changed(&self, row_id: RowId) { if !self.filters.read().await.is_empty() { self @@ -281,38 +219,14 @@ impl FilterController { FilterChangeset::Delete { filter_id, field_id: _, - } => { - for (position, filter) in filters.iter_mut().enumerate() { - if filter.id == filter_id { - filters.remove(position); - break; - } - let parent_filter = filter.find_parent_of_filter(&filter_id); - if let Some(filter) = parent_filter { - let result = filter.delete_filter(&filter_id); - if result.is_ok() { - break; - } - } - } - }, + } => Self::delete_filter(&mut filters, &filter_id), FilterChangeset::DeleteAllWithFieldId { field_id } => { - let mut filter_ids: Vec = vec![]; - for filter in filters.iter_mut() { + let mut filter_ids = vec![]; + for filter in filters.iter() { filter.find_all_filters_with_field_id(&field_id, &mut filter_ids); } - for filter_id in filter_ids { - for (position, filter) in filters.iter_mut().enumerate() { - if filter.id == filter_id { - filters.remove(position); - break; - } - let parent_filter = filter.find_parent_of_filter(&filter_id); - if let Some(filter) = parent_filter { - let _ = filter.delete_filter(&filter_id); - } - } + Self::delete_filter(&mut filters, &filter_id) } }, } @@ -325,6 +239,210 @@ impl FilterController { FilterChangesetNotificationPB::from_filters(&self.view_id, &filters) } + + pub async fn fill_cells(&self, cells: &mut Cells) -> bool { + let filters = self.filters.read().await; + + let mut open_after_create = false; + + let mut min_required_filters: Vec<&FilterInner> = vec![]; + for filter in filters.iter() { + filter.get_min_effective_filters(&mut min_required_filters); + } + + let field_map = self.get_field_map().await; + + while let Some(current_inner) = min_required_filters.pop() { + if let FilterInner::Data { + field_id, + field_type, + condition_and_content, + } = ¤t_inner + { + if min_required_filters.iter().any( + |inner| matches!(inner, FilterInner::Data { field_id: other_id, .. } if other_id == field_id), + ) { + min_required_filters.retain( + |inner| matches!(inner, FilterInner::Data { field_id: other_id, .. } if other_id != field_id), + ); + open_after_create = true; + continue; + } + + if let Some(field) = field_map.get(field_id) { + let (cell, flag) = match field_type { + FieldType::RichText | FieldType::URL => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Number => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::DateTime => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::SingleSelect => { + let filter = condition_and_content + .cloned::() + .unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::MultiSelect => { + let filter = condition_and_content + .cloned::() + .unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Checkbox => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Checklist => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + _ => (None, false), + }; + + if let Some(cell) = cell { + cells.insert(field_id.clone(), cell); + } + + if flag { + open_after_create = flag; + } + } + } + } + + open_after_create + } + + #[tracing::instrument( + name = "process_filter_task", + level = "trace", + skip_all, + fields(filter_result), + err + )] + pub async fn process(&self, predicate: &str) -> FlowyResult<()> { + let event_type = FilterEvent::from_str(predicate).unwrap(); + match event_type { + FilterEvent::FilterDidChanged => self.filter_all_rows_handler().await?, + FilterEvent::RowDidChanged(row_id) => self.filter_single_row_handler(row_id).await?, + } + Ok(()) + } + + async fn filter_single_row_handler(&self, row_id: RowId) -> FlowyResult<()> { + let filters = self.filters.read().await; + + if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await { + let field_by_field_id = self.get_field_map().await; + let mut notification = FilterResultNotification::new(self.view_id.clone()); + if let Some(is_visible) = filter_row( + &row_detail.row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &filters, + ) { + if is_visible { + if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { + notification.visible_rows.push( + InsertedRowPB::new(RowMetaPB::from(row_detail.as_ref())).with_index(index as i32), + ) + } + } else { + notification.invisible_rows.push(row_id); + } + } + + let _ = self + .notifier + .send(DatabaseViewChanged::FilterNotification(notification)); + } + Ok(()) + } + + async fn filter_all_rows_handler(&self) -> FlowyResult<()> { + let filters = self.filters.read().await; + + let field_by_field_id = self.get_field_map().await; + let mut visible_rows = vec![]; + let mut invisible_rows = vec![]; + + for (index, row_detail) in self + .delegate + .get_rows(&self.view_id) + .await + .into_iter() + .enumerate() + { + if let Some(is_visible) = filter_row( + &row_detail.row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &filters, + ) { + if is_visible { + let row_meta = RowMetaPB::from(row_detail.as_ref()); + visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) + } else { + invisible_rows.push(row_detail.row.id.clone()); + } + } + } + + let notification = FilterResultNotification { + view_id: self.view_id.clone(), + invisible_rows, + visible_rows, + }; + tracing::trace!("filter result {:?}", filters); + let _ = self + .notifier + .send(DatabaseViewChanged::FilterNotification(notification)); + + Ok(()) + } + + async fn get_field_map(&self) -> HashMap { + self + .delegate + .get_fields(&self.view_id, None) + .await + .into_iter() + .map(|field| (field.id.clone(), field)) + .collect::>() + } + + fn delete_filter(filters: &mut Vec, filter_id: &str) { + let mut find_root_filter: Option = None; + let mut find_parent_of_non_root_filter: Option<&mut Filter> = None; + + for (position, filter) in filters.iter_mut().enumerate() { + if filter.id == filter_id { + find_root_filter = Some(position); + break; + } + if let Some(filter) = filter.find_parent_of_filter(filter_id) { + find_parent_of_non_root_filter = Some(filter); + break; + } + } + + if let Some(pos) = find_root_filter { + filters.remove(pos); + } else if let Some(filter) = find_parent_of_non_root_filter { + if let Err(err) = filter.delete_filter(filter_id) { + tracing::error!("error while deleting filter: {}", err); + } + } + } } /// Returns `Some` if the visibility of the row changed after applying the filter and `None` @@ -336,22 +454,28 @@ fn filter_row( field_by_field_id: &HashMap, cell_data_cache: &CellCache, filters: &Vec, -) -> Option<(RowId, bool)> { +) -> Option { // Create a filter result cache if it doesn't exist let mut filter_result = result_by_row_id.entry(row.id.clone()).or_insert(true); let old_is_visible = *filter_result; let mut new_is_visible = true; + for filter in filters { if let Some(is_visible) = apply_filter(row, field_by_field_id, cell_data_cache, filter) { new_is_visible = new_is_visible && is_visible; + + // short-circuit as soon as one filter tree returns false + if !new_is_visible { + break; + } } } *filter_result = new_is_visible; if old_is_visible != new_is_visible { - Some((row.id.clone(), new_is_visible)) + Some(new_is_visible) } else { None } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 980b69a7aa..1d30c5949c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::mem; use anyhow::bail; @@ -25,7 +26,8 @@ pub struct Filter { } impl Filter { - /// Recursively determine whether there are any data filters in the filter tree. + /// Recursively determine whether there are any data filters in the filter tree. A tree that has + /// multiple AND/OR filters but no Data filters is considered "empty". pub fn is_empty(&self) -> bool { match &self.inner { FilterInner::And { children } | FilterInner::Or { children } => children @@ -36,6 +38,7 @@ impl Filter { } } + /// Recursively find a filter based on `filter_id`. Returns `None` if the filter cannot be found. pub fn find_filter(&mut self, filter_id: &str) -> Option<&mut Self> { if self.id == filter_id { return Some(self); @@ -54,6 +57,8 @@ impl Filter { } } + /// Recursively find the parent of a filter whose id is `filter_id`. Returns `None` if the filter + /// cannot be found. pub fn find_parent_of_filter(&mut self, filter_id: &str) -> Option<&mut Self> { if self.id == filter_id { return None; @@ -75,7 +80,7 @@ impl Filter { } } - /// converts a filter from And/Or/Data to And/Or. If the current type of the filter is Data, + /// Converts a filter from And/Or/Data to And/Or. If the current type of the filter is Data, /// return the FilterInner after the conversion. pub fn convert_to_and_or_filter_type( &mut self, @@ -118,6 +123,10 @@ impl Filter { } } + /// Insert a filter into the current filter in the filter tree. If the current filter + /// is an AND/OR filter, then the filter is appended to its children. Otherwise, the current + /// filter is converted to an AND filter, after which the current data filter and the new filter + /// are added to the AND filter's children. pub fn insert_filter(&mut self, filter: Filter) -> FlowyResult<()> { match &mut self.inner { FilterInner::And { children } | FilterInner::Or { children } => { @@ -141,6 +150,8 @@ impl Filter { Ok(()) } + /// Update the criteria of a data filter. Return an error if the current filter is an AND/OR + /// filter. pub fn update_filter_data(&mut self, filter_data: FilterInner) -> FlowyResult<()> { match &self.inner { FilterInner::And { .. } | FilterInner::Or { .. } => Err(FlowyError::internal().with_context( @@ -153,6 +164,9 @@ impl Filter { } } + /// Delete a filter based on `filter_id`. The current filter must be the parent of the filter + /// whose id is `filter_id`. Returns an error if the current filter is a Data filter (which + /// cannot have children), or the filter to be deleted cannot be found. pub fn delete_filter(&mut self, filter_id: &str) -> FlowyResult<()> { match &mut self.inner { FilterInner::And { children } | FilterInner::Or { children } => children @@ -171,24 +185,63 @@ impl Filter { } } - pub fn find_all_filters_with_field_id(&mut self, matching_field_id: &str, ids: &mut Vec) { - match &mut self.inner { + /// Recursively finds any Data filter whose `field_id` is equal to `matching_field_id`. Any found + /// filters' id is appended to the `ids` vector. + pub fn find_all_filters_with_field_id(&self, matching_field_id: &str, ids: &mut Vec) { + match &self.inner { FilterInner::And { children } | FilterInner::Or { children } => { - for child_filter in children.iter_mut() { + for child_filter in children.iter() { child_filter.find_all_filters_with_field_id(matching_field_id, ids); } }, - FilterInner::Data { - field_id, - field_type: _, - condition_and_content: _, - } => { + FilterInner::Data { field_id, .. } => { if field_id == matching_field_id { ids.push(self.id.clone()); } }, } } + + /// Recursively determine the smallest set of filters that loosely represents the filter tree. The + /// filters are appended to the `min_effective_filters` vector. The following rules are followed + /// when determining if a filter should get included. If the current filter is: + /// + /// 1. a Data filter, then it should be included. + /// 2. an AND filter, then all of its effective children should be + /// included. + /// 3. an OR filter, then only the first child should be included. + pub fn get_min_effective_filters<'a>(&'a self, min_effective_filters: &mut Vec<&'a FilterInner>) { + match &self.inner { + FilterInner::And { children } => { + for filter in children.iter() { + filter.get_min_effective_filters(min_effective_filters); + } + }, + FilterInner::Or { children } => { + if let Some(filter) = children.first() { + filter.get_min_effective_filters(min_effective_filters); + } + }, + FilterInner::Data { .. } => min_effective_filters.push(&self.inner), + } + } + + /// Recursively get all of the filtering field ids and the associated filter_ids + pub fn get_all_filtering_field_ids(&self, field_ids: &mut HashMap>) { + match &self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + for child in children.iter() { + child.get_all_filtering_field_ids(field_ids); + } + }, + FilterInner::Data { field_id, .. } => { + field_ids + .entry(field_id.clone()) + .and_modify(|filter_ids| filter_ids.push(self.id.clone())) + .or_insert_with(|| vec![self.id.clone()]); + }, + } + } } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index c4ad1f79ea..b42ac89b39 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -117,6 +117,7 @@ impl SortController { pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { let event_type = SortEvent::from_str(predicate).unwrap(); let mut row_details = self.delegate.get_rows(&self.view_id).await; + match event_type { SortEvent::SortDidChanged | SortEvent::DeleteAllSorts => { self.sort_rows(&mut row_details).await; diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 8e4af7073f..cccaba68fe 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -155,12 +155,13 @@ impl DatabaseEditorTest { type_option.options } - pub fn get_single_select_type_option(&self, field_id: &str) -> SingleSelectTypeOption { + pub fn get_single_select_type_option(&self, field_id: &str) -> Vec { let field_type = FieldType::SingleSelect; let field = self.get_field(field_id, field_type); - field + let type_option = field .get_type_option::(field_type) - .unwrap() + .unwrap(); + type_option.options } #[allow(dead_code)] diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index bc54883697..3ec982f461 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -1,7 +1,7 @@ use collab_database::database::gen_option_id; use flowy_database2::entities::{FieldChangesetParams, FieldType}; -use flowy_database2::services::field::{SelectOption, CHECK, UNCHECK}; +use flowy_database2::services::field::{SelectOption, SingleSelectTypeOption, CHECK, UNCHECK}; use crate::database::field_test::script::DatabaseFieldTest; use crate::database::field_test::script::FieldScript::*; @@ -104,16 +104,16 @@ async fn grid_switch_from_select_option_to_checkbox_test() { let field = test.get_first_field(FieldType::SingleSelect); // Update the type option data of single select option - let mut single_select_type_option = test.get_single_select_type_option(&field.id); - single_select_type_option.options.clear(); + let mut options = test.get_single_select_type_option(&field.id); + options.clear(); // Add a new option with name CHECK - single_select_type_option.options.push(SelectOption { + options.push(SelectOption { id: gen_option_id(), name: CHECK.to_string(), color: Default::default(), }); // Add a new option with name UNCHECK - single_select_type_option.options.push(SelectOption { + options.push(SelectOption { id: gen_option_id(), name: UNCHECK.to_string(), color: Default::default(), @@ -122,7 +122,11 @@ async fn grid_switch_from_select_option_to_checkbox_test() { let scripts = vec![ UpdateTypeOption { field_id: field.id.clone(), - type_option: single_select_type_option.into(), + type_option: SingleSelectTypeOption { + options, + disable_color: false, + } + .into(), }, SwitchToField { field_id: field.id.clone(), @@ -159,16 +163,10 @@ async fn grid_switch_from_checkbox_to_select_option_test() { ]; test.run_scripts(scripts).await; - let single_select_type_option = test.get_single_select_type_option(&checkbox_field.id); - assert_eq!(single_select_type_option.options.len(), 2); - assert!(single_select_type_option - .options - .iter() - .any(|option| option.name == UNCHECK)); - assert!(single_select_type_option - .options - .iter() - .any(|option| option.name == CHECK)); + let options = test.get_single_select_type_option(&checkbox_field.id); + assert_eq!(options.len(), 2); + assert!(options.iter().any(|option| option.name == UNCHECK)); + assert!(options.iter().any(|option| option.name == CHECK)); } // Test when switching the current field from Multi-select to Text test diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs index 73a0bfc191..eb808d0bc3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -107,7 +107,7 @@ async fn grid_filter_single_select_is_empty_test() { async fn grid_filter_single_select_is_test() { let mut test = DatabaseFilterTest::new().await; let field = test.get_first_field(FieldType::SingleSelect); - let mut options = test.get_single_select_type_option(&field.id).options; + let mut options = test.get_single_select_type_option(&field.id); let expected = 2; let row_count = test.row_details.len(); let scripts = vec![ @@ -133,7 +133,7 @@ async fn grid_filter_single_select_is_test2() { let mut test = DatabaseFilterTest::new().await; let field = test.get_first_field(FieldType::SingleSelect); let row_details = test.get_rows().await; - let mut options = test.get_single_select_type_option(&field.id).options; + let mut options = test.get_single_select_type_option(&field.id); let option = options.remove(0); let row_count = test.row_details.len(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index e87cec40e6..01362d75b4 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -117,11 +117,7 @@ pub fn make_test_grid() -> DatabaseData { fields.push(url); }, FieldType::Checklist => { - // let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple); - // let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange); - // let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow); let type_option = ChecklistTypeOption; - // type_option.options.extend(vec![option1, option2, option3]); let checklist_field = FieldBuilder::new(field_type, type_option) .name("TODO") .visibility(true) diff --git a/frontend/rust-lib/flowy-database2/tests/database/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mod.rs index 5333d54c33..f1614f5493 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mod.rs @@ -7,7 +7,7 @@ mod field_test; mod filter_test; mod group_test; mod layout_test; -mod sort_test; - mod mock_data; +mod pre_fill_cell_test; mod share_test; +mod sort_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs new file mode 100644 index 0000000000..0e76b61079 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs @@ -0,0 +1,3 @@ +mod pre_fill_row_according_to_filter_test; +mod pre_fill_row_with_payload_test; +mod script; diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs new file mode 100644 index 0000000000..1000df6f4b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs @@ -0,0 +1,433 @@ +use flowy_database2::entities::{ + CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType, + FilterDataPB, SelectOptionFilterConditionPB, SelectOptionFilterPB, TextFilterConditionPB, + TextFilterPB, +}; +use flowy_database2::services::field::SELECTION_IDS_SEPARATOR; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating an empty row into a database that has +// active filters. Where appropriate, the row's cell data will be pre-filled +// into the row's cells before creating it in collab. + +#[tokio::test] +async fn according_to_text_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "sample".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len() - 1, + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len() - 1, + from_field_type: FieldType::RichText, + expected_content: "sample".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_empty_text_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len() - 1, + exists: false, + }]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_text_is_not_empty_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextIsNotEmpty, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(6), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_checkbox_is_unchecked_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsUnChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(4), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(5), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: 4, + exists: false, + }]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_checkbox_is_checked_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(3), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(4), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: 3, + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id, + row_index: 3, + from_field_type: FieldType::Checkbox, + expected_content: "Yes".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_date_time_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let datetime_field = test.get_first_field(FieldType::DateTime); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: Some(1710510086), + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(0), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(1), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: datetime_field.id.clone(), + row_index: 0, + exists: true, + }, + AssertCellContent { + field_id: datetime_field.id, + row_index: 0, + from_field_type: FieldType::DateTime, + expected_content: "2024/03/15".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_invalid_date_time_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let datetime_field = test.get_first_field(FieldType::DateTime); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: None, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(7), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(8), + AssertCellExistence { + field_id: datetime_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let filtering_options = vec![options[1].clone(), options[2].clone()]; + let ids = filtering_options + .iter() + .map(|option| option.id.clone()) + .collect(); + let stringified_expected = filtering_options + .iter() + .map(|option| option.name.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(1), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(2), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 1, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 1, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let filtering_options = vec![options[1].clone(), options[2].clone()]; + let ids = filtering_options + .iter() + .map(|option| option.id.clone()) + .collect(); + let stringified_expected = filtering_options.first().unwrap().name.clone(); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_is_not_empty_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let stringified_expected = options.first().unwrap().name.clone(); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs new file mode 100644 index 0000000000..b1b42d6479 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs @@ -0,0 +1,422 @@ +use std::collections::HashMap; + +use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; +use flowy_database2::services::field::{DateCellData, SELECTION_IDS_SEPARATOR}; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating a row using `CreateRowPayloadPB` that passes +// in some cell data in its `data` field of `HashMap` which is a +// map of `field_id` to its corresponding cell data as a String. If valid, the cell +// data will be pre-filled into the row's cells before creating it in collab. + +#[tokio::test] +async fn row_data_payload_with_empty_hashmap_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::new(), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_unknown_field_id_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let malformed_field_id = "this_field_id_will_never_exist"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([( + malformed_field_id.to_string(), + "sample cell data".to_string(), + )]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "".to_string(), + }, + AssertCellExistence { + field_id: malformed_field_id.to_string(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_empty_string_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let cell_data = ""; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let cell_data = "sample cell data"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_multi_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let number_field = test.get_first_field(FieldType::Number); + let url_field = test.get_first_field(FieldType::URL); + + let text_cell_data = "sample cell data"; + let number_cell_data = "1234"; + let url_cell_data = "appflowy.io"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([ + (text_field.id.clone(), text_cell_data.to_string()), + (number_field.id.clone(), number_cell_data.to_string()), + (url_field.id.clone(), url_cell_data.to_string()), + ]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: text_cell_data.to_string(), + }, + AssertCellExistence { + field_id: number_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: number_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "$1,234".to_string(), + }, + AssertCellExistence { + field_id: url_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: url_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: url_cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_date_time_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let date_field = test.get_first_field(FieldType::DateTime); + let cell_data = "1710510086"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "2024/03/15".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_invalid_date_time_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let date_field = test.get_first_field(FieldType::DateTime); + let cell_data = DateCellData { + timestamp: Some(1710510086), + ..Default::default() + } + .to_string(); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_checkbox_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let cell_data = "Yes"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(checkbox_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::Checkbox, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_select_option_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let stringified_cell_data = options + .iter() + .map(|option| option.name.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids)]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::MultiSelect, + expected_content: stringified_cell_data, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_invalid_select_option_id_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&multi_select_field.id); + + let first_id = options.swap_remove(0).id; + let ids = [first_id.clone(), "nonsense".to_string()].join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: first_id, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_too_many_select_option_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let single_select_field = test.get_first_field(FieldType::SingleSelect); + let mut options = test.get_single_select_type_option(&single_select_field.id); + + let ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let stringified_cell_data = options.swap_remove(0).id; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(single_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: stringified_cell_data, + }, + ]; + + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs new file mode 100644 index 0000000000..e78732ec51 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs @@ -0,0 +1,164 @@ +use std::ops::{Deref, DerefMut}; +use std::time::Duration; + +use flowy_database2::entities::{CreateRowPayloadPB, FieldType, FilterDataPB, InsertFilterPB}; +use flowy_database2::services::cell::stringify_cell_data; +use flowy_database2::services::field::{SelectOptionIds, SELECTION_IDS_SEPARATOR}; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum PreFillRowCellTestScript { + CreateEmptyRow, + CreateRowWithPayload { + payload: CreateRowPayloadPB, + }, + InsertFilter { + filter: FilterDataPB, + }, + AssertRowCount(usize), + AssertCellExistence { + field_id: String, + row_index: usize, + exists: bool, + }, + AssertCellContent { + field_id: String, + row_index: usize, + from_field_type: FieldType, + expected_content: String, + }, + AssertSelectOptionCellStrict { + field_id: String, + row_index: usize, + expected_content: String, + }, + Wait { + milliseconds: u64, + }, +} + +pub struct DatabasePreFillRowCellTest { + inner: DatabaseEditorTest, +} + +impl DatabasePreFillRowCellTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { inner: editor_test } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: PreFillRowCellTestScript) { + match script { + PreFillRowCellTestScript::CreateEmptyRow => { + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::CreateRowWithPayload { payload } => { + let row_detail = self.editor.create_row(payload).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::InsertFilter { filter } => self + .editor + .modify_view_filters( + &self.view_id, + InsertFilterPB { + parent_filter_id: None, + data: filter, + } + .try_into() + .unwrap(), + ) + .await + .unwrap(), + PreFillRowCellTestScript::AssertRowCount(expected_row_count) => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + assert_eq!(expected_row_count, rows.len()); + }, + PreFillRowCellTestScript::AssertCellExistence { + field_id, + row_index, + exists, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail.row.cells.get(&field_id).cloned(); + + assert_eq!(exists, cell.is_some()); + }, + PreFillRowCellTestScript::AssertCellContent { + field_id, + row_index, + from_field_type, + expected_content, + } => { + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::AssertSelectOptionCellStrict { + field_id, + row_index, + expected_content, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + + let content = SelectOptionIds::from(&cell).join(SELECTION_IDS_SEPARATOR); + + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::Wait { milliseconds } => { + tokio::time::sleep(Duration::from_millis(milliseconds)).await; + }, + } + } +} + +impl Deref for DatabasePreFillRowCellTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DatabasePreFillRowCellTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +}