feat: pre-fill row cells with filter data before row creation (#4854)

* feat: fill cells according to active filters

* chore: short circuit filter_row function

* chore: delete corresponding filters when filtered filter is deleted

* chore: validate filters when loading

* chore: remove unnecessary tuple in return

* chore: use trait

* chore: add tests
This commit is contained in:
Richard Shiue 2024-03-16 17:18:30 +08:00 committed by GitHub
parent 452974ab99
commit 0f006fa60b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1557 additions and 184 deletions

View File

@ -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,
}

View File

@ -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<Cell>, bool) {
(None, false)
}
}

View File

@ -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;
}
}

View File

@ -17,7 +17,6 @@ pub async fn make_filter_controller(
notifier: DatabaseViewChangedNotifier,
cell_cache: CellCache,
) -> Arc<FilterController> {
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<Filter> {
self.0.get_all_filters(view_id)
}
fn save_filters(&self, view_id: &str, filters: &[Filter]) {
self.0.save_filters(view_id, filters)
}

View File

@ -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<Cell>, 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};

View File

@ -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<Cell>, bool) {
(None, true)
}
}

View File

@ -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<Cell>, 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};

View File

@ -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<bool> {
@ -30,6 +34,39 @@ impl NumberFilterPB {
}
}
impl PreFillCellsWithFilter for NumberFilterPB {
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, 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),

View File

@ -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<bool> {
@ -90,6 +95,40 @@ impl SelectOptionFilterStrategy {
}
}
impl PreFillCellsWithFilter for SelectOptionFilterPB {
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, 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};

View File

@ -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<T: AsRef<str>>(&self, cell_data: T) -> bool {
@ -17,6 +21,26 @@ impl TextFilterPB {
}
}
impl PreFillCellsWithFilter for TextFilterPB {
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, 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)]

View File

@ -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<Bytes, Error = ProtobufError> + 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.
///

View File

@ -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<Vec<String>>) -> Fut<Vec<Field>>;
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>;
fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>>;
fn get_all_filters(&self, view_id: &str) -> Vec<Filter>;
fn save_filters(&self, view_id: &str, filters: &[Filter]);
}
pub trait PreFillCellsWithFilter {
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool);
}
pub struct FilterController {
view_id: String,
handler_id: String,
@ -51,13 +56,46 @@ impl FilterController {
handler_id: &str,
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
filters: Vec<Filter>,
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::<Vec<_>>();
let mut need_save = false;
let mut filters = delegate.get_all_filters(view_id);
let mut filtering_field_ids: HashMap<String, Vec<String>> = 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<String, Field> {
self
.delegate
.get_fields(&self.view_id, None)
.await
.into_iter()
.map(|field| (field.id.clone(), field))
.collect::<HashMap<String, Field>>()
}
#[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<String> = 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,
} = &current_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::<TextFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
FieldType::Number => {
let filter = condition_and_content.cloned::<NumberFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
FieldType::DateTime => {
let filter = condition_and_content.cloned::<DateFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
FieldType::SingleSelect => {
let filter = condition_and_content
.cloned::<SelectOptionFilterPB>()
.unwrap();
filter.get_compliant_cell(field)
},
FieldType::MultiSelect => {
let filter = condition_and_content
.cloned::<SelectOptionFilterPB>()
.unwrap();
filter.get_compliant_cell(field)
},
FieldType::Checkbox => {
let filter = condition_and_content.cloned::<CheckboxFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
FieldType::Checklist => {
let filter = condition_and_content.cloned::<ChecklistFilterPB>().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<String, Field> {
self
.delegate
.get_fields(&self.view_id, None)
.await
.into_iter()
.map(|field| (field.id.clone(), field))
.collect::<HashMap<String, Field>>()
}
fn delete_filter(filters: &mut Vec<Filter>, filter_id: &str) {
let mut find_root_filter: Option<usize> = 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<String, Field>,
cell_data_cache: &CellCache,
filters: &Vec<Filter>,
) -> Option<(RowId, bool)> {
) -> Option<bool> {
// 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
}

View File

@ -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<String>) {
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<String>) {
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<String, Vec<String>>) {
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)]

View File

@ -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;

View File

@ -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<SelectOption> {
let field_type = FieldType::SingleSelect;
let field = self.get_field(field_id, field_type);
field
let type_option = field
.get_type_option::<SingleSelectTypeOption>(field_type)
.unwrap()
.unwrap();
type_option.options
}
#[allow(dead_code)]

View File

@ -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

View File

@ -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();

View File

@ -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)

View File

@ -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;

View File

@ -0,0 +1,3 @@
mod pre_fill_row_according_to_filter_test;
mod pre_fill_row_with_payload_test;
mod script;

View File

@ -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::<Vec<_>>()
.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;
}

View File

@ -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<String, String>` 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::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR);
let stringified_cell_data = options
.iter()
.map(|option| option.name.clone())
.collect::<Vec<_>>()
.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::<Vec<_>>()
.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;
}

View File

@ -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<PreFillRowCellTestScript>) {
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
}
}