mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
452974ab99
commit
0f006fa60b
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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),
|
||||
|
@ -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};
|
||||
|
@ -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)]
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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,
|
||||
} = ¤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::<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
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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;
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,3 @@
|
||||
mod pre_fill_row_according_to_filter_test;
|
||||
mod pre_fill_row_with_payload_test;
|
||||
mod script;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user