feat: sort cell (#1593)

* chore: call cell decode data

* chore: cache cell decoded data

* chore: update cache cell data

* chore: cache cell data

* refactor: separate cell type option functionalities

* refactor: add TypeOptionCellDataFilter trait

* chore: remove unused codes

* chore: fix wanrings

* chore: add sort tests

* chore: sort single select and multi select

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2022-12-24 23:19:11 +08:00 committed by GitHub
parent 6a465ac3e7
commit 5a30f46b85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1628 additions and 849 deletions

View File

@ -63,7 +63,7 @@ class DateCellDataPersistence
CellPathPB _makeCellPath(GridCellIdentifier cellId) {
return CellPathPB.create()
..gridId = cellId.gridId
..viewId = cellId.gridId
..fieldId = cellId.fieldId
..rowId = cellId.rowId;
}

View File

@ -46,7 +46,7 @@ class CellService {
required GridCellIdentifier cellId,
}) {
final payload = CellPathPB.create()
..gridId = cellId.gridId
..viewId = cellId.gridId
..fieldId = cellId.fieldId
..rowId = cellId.rowId;
return GridEventGetCell(payload).send();

View File

@ -23,7 +23,7 @@ class SelectOptionFFIService {
return result.fold(
(option) {
final cellIdentifier = CellPathPB.create()
..gridId = gridId
..viewId = gridId
..fieldId = fieldId
..rowId = rowId;
final payload = SelectOptionChangesetPB.create()
@ -62,7 +62,7 @@ class SelectOptionFFIService {
Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
final payload = CellPathPB.create()
..gridId = gridId
..viewId = gridId
..fieldId = fieldId
..rowId = rowId;
@ -87,7 +87,7 @@ class SelectOptionFFIService {
CellPathPB _cellIdentifier() {
return CellPathPB.create()
..gridId = gridId
..viewId = gridId
..fieldId = fieldId
..rowId = rowId;
}

View File

@ -2706,7 +2706,7 @@ final Map<String, String> flags = Map.fromIterables([
'Flag: Armenia',
'Flag: Angola',
'Flag: Antarctica',
'Flag: Argentina',
'Flag: argentina',
'Flag: American Samoa',
'Flag: Austria',
'Flag: Australia',
@ -2775,7 +2775,7 @@ final Map<String, String> flags = Map.fromIterables([
'Flag: Falkland Islands',
'Flag: Micronesia',
'Flag: Faroe Islands',
'Flag: France',
'Flag: france',
'Flag: Gabon',
'Flag: United Kingdom',
'Flag: Grenada',

View File

@ -2706,7 +2706,7 @@ final Map<String, String> flags = Map.fromIterables([
'Flag: Armenia',
'Flag: Angola',
'Flag: Antarctica',
'Flag: Argentina',
'Flag: argentina',
'Flag: American Samoa',
'Flag: Austria',
'Flag: Australia',
@ -2775,7 +2775,7 @@ final Map<String, String> flags = Map.fromIterables([
'Flag: Falkland Islands',
'Flag: Micronesia',
'Flag: Faroe Islands',
'Flag: France',
'Flag: france',
'Flag: Gabon',
'Flag: United Kingdom',
'Flag: Grenada',

View File

@ -1021,6 +1021,7 @@ dependencies = [
"lib-infra",
"lib-ot",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"rayon",
"regex",

View File

@ -45,6 +45,7 @@ futures = "0.3.15"
atomic_refcell = "0.1.8"
crossbeam-utils = "0.8.7"
async-stream = "0.3.2"
parking_lot = "0.12.1"
[dev-dependencies]
flowy-test = { path = "../flowy-test" }

View File

@ -41,7 +41,7 @@ impl TryInto<CreateSelectOptionParams> for CreateSelectOptionPayloadPB {
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CellPathPB {
#[pb(index = 1)]
pub grid_id: String,
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
@ -50,6 +50,8 @@ pub struct CellPathPB {
pub row_id: String,
}
/// Represents as the cell identifier. It's used to locate the cell in corresponding
/// view's row with the field id.
pub struct CellPathParams {
pub view_id: String,
pub field_id: String,
@ -60,7 +62,7 @@ impl TryInto<CellPathParams> for CellPathPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CellPathParams, Self::Error> {
let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
let grid_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
Ok(CellPathParams {
@ -70,15 +72,19 @@ impl TryInto<CellPathParams> for CellPathPB {
})
}
}
/// Represents as the data of the cell.
#[derive(Debug, Default, ProtoBuf)]
pub struct CellPB {
#[pb(index = 1)]
pub field_id: String,
// The data was encoded in field_type's data type
/// Encoded the data using the helper struct `CellProtobufBlob`.
/// Check out the `CellProtobufBlob` for more information.
#[pb(index = 2)]
pub data: Vec<u8>,
/// the field_type will be None if the field with field_id is not found
#[pb(index = 3, one_of)]
pub field_type: Option<FieldType>,
}

View File

@ -1,3 +1,4 @@
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -39,6 +40,18 @@ impl std::convert::TryFrom<u8> for CheckboxFilterConditionPB {
}
}
impl FromFilterString for CheckboxFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::try_from(filter_rev.condition)
.unwrap_or(CheckboxFilterConditionPB::IsChecked),
}
}
}
impl std::convert::From<&FilterRevision> for CheckboxFilterPB {
fn from(rev: &FilterRevision) -> Self {
CheckboxFilterPB {

View File

@ -1,3 +1,4 @@
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -39,6 +40,18 @@ impl std::convert::TryFrom<u8> for ChecklistFilterConditionPB {
}
}
impl FromFilterString for ChecklistFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
ChecklistFilterPB {
condition: ChecklistFilterConditionPB::try_from(filter_rev.condition)
.unwrap_or(ChecklistFilterConditionPB::IsIncomplete),
}
}
}
impl std::convert::From<&FilterRevision> for ChecklistFilterPB {
fn from(rev: &FilterRevision) -> Self {
ChecklistFilterPB {

View File

@ -1,3 +1,4 @@
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -80,6 +81,26 @@ impl std::convert::TryFrom<u8> for DateFilterConditionPB {
}
}
}
impl FromFilterString for DateFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
let condition = DateFilterConditionPB::try_from(filter_rev.condition).unwrap_or(DateFilterConditionPB::DateIs);
let mut filter = DateFilterPB {
condition,
..Default::default()
};
if let Ok(content) = DateFilterContentPB::from_str(&filter_rev.content) {
filter.start = content.start;
filter.end = content.end;
filter.timestamp = content.timestamp;
};
filter
}
}
impl std::convert::From<&FilterRevision> for DateFilterPB {
fn from(rev: &FilterRevision) -> Self {
let condition = DateFilterConditionPB::try_from(rev.condition).unwrap_or(DateFilterConditionPB::DateIs);

View File

@ -1,3 +1,4 @@
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -53,6 +54,18 @@ impl std::convert::TryFrom<u8> for NumberFilterConditionPB {
}
}
impl FromFilterString for NumberFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
NumberFilterPB {
condition: NumberFilterConditionPB::try_from(filter_rev.condition)
.unwrap_or(NumberFilterConditionPB::Equal),
content: filter_rev.content.clone(),
}
}
}
impl std::convert::From<&FilterRevision> for NumberFilterPB {
fn from(rev: &FilterRevision) -> Self {
NumberFilterPB {

View File

@ -1,4 +1,5 @@
use crate::services::field::SelectOptionIds;
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -46,6 +47,19 @@ impl std::convert::TryFrom<u8> for SelectOptionConditionPB {
}
}
}
impl FromFilterString for SelectOptionFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
let ids = SelectOptionIds::from(filter_rev.content.clone());
SelectOptionFilterPB {
condition: SelectOptionConditionPB::try_from(filter_rev.condition)
.unwrap_or(SelectOptionConditionPB::OptionIs),
option_ids: ids.into_inner(),
}
}
}
impl std::convert::From<&FilterRevision> for SelectOptionFilterPB {
fn from(rev: &FilterRevision) -> Self {

View File

@ -1,3 +1,4 @@
use crate::services::filter::FromFilterString;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
@ -54,6 +55,18 @@ impl std::convert::TryFrom<u8> for TextFilterConditionPB {
}
}
impl FromFilterString for TextFilterPB {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized,
{
TextFilterPB {
condition: TextFilterConditionPB::try_from(filter_rev.condition).unwrap_or(TextFilterConditionPB::Is),
content: filter_rev.content.clone(),
}
}
}
impl std::convert::From<&FilterRevision> for TextFilterPB {
fn from(rev: &FilterRevision) -> Self {
TextFilterPB {

View File

@ -130,7 +130,7 @@ impl TryInto<DeleteSortParams> for DeleteSortPayloadPB {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DeleteSortParams {
pub view_id: String,
pub sort_type: SortType,

View File

@ -0,0 +1,121 @@
use parking_lot::RwLock;
use std::any::{type_name, Any};
use std::collections::HashMap;
use crate::services::filter::FilterType;
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::Arc;
pub type AtomicCellDataCache = Arc<RwLock<AnyTypeCache<u64>>>;
pub type AtomicCellFilterCache = Arc<RwLock<AnyTypeCache<FilterType>>>;
#[derive(Default, Debug)]
pub struct AnyTypeCache<TypeValueKey>(HashMap<TypeValueKey, TypeValue>);
impl<TypeValueKey> AnyTypeCache<TypeValueKey>
where
TypeValueKey: Clone + Hash + Eq,
{
pub fn new() -> Arc<RwLock<AnyTypeCache<TypeValueKey>>> {
Arc::new(RwLock::new(AnyTypeCache(HashMap::default())))
}
pub fn insert<T>(&mut self, key: &TypeValueKey, val: T) -> Option<T>
where
T: 'static + Send + Sync,
{
self.0.insert(key.clone(), TypeValue::new(val)).and_then(downcast_owned)
}
pub fn remove(&mut self, key: &TypeValueKey) {
self.0.remove(key);
}
// pub fn remove<T, K: AsRef<TypeValueKey>>(&mut self, key: K) -> Option<T>
// where
// T: 'static + Send + Sync,
// {
// self.0.remove(key.as_ref()).and_then(downcast_owned)
// }
pub fn get<T>(&self, key: &TypeValueKey) -> Option<&T>
where
T: 'static + Send + Sync,
{
self.0.get(key).and_then(|type_value| type_value.boxed.downcast_ref())
}
pub fn get_mut<T>(&mut self, key: &TypeValueKey) -> Option<&mut T>
where
T: 'static + Send + Sync,
{
self.0
.get_mut(key)
.and_then(|type_value| type_value.boxed.downcast_mut())
}
pub fn contains(&self, key: &TypeValueKey) -> bool {
self.0.contains_key(key)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
fn downcast_owned<T: 'static + Send + Sync>(type_value: TypeValue) -> Option<T> {
type_value.boxed.downcast().ok().map(|boxed| *boxed)
}
#[derive(Debug)]
struct TypeValue {
boxed: Box<dyn Any + Send + Sync + 'static>,
#[allow(dead_code)]
ty: &'static str,
}
impl TypeValue {
pub fn new<T>(value: T) -> Self
where
T: Send + Sync + 'static,
{
Self {
boxed: Box::new(value),
ty: type_name::<T>(),
}
}
}
impl std::ops::Deref for TypeValue {
type Target = Box<dyn Any + Send + Sync + 'static>;
fn deref(&self) -> &Self::Target {
&self.boxed
}
}
impl std::ops::DerefMut for TypeValue {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.boxed
}
}
// #[cfg(test)]
// mod tests {
// use crate::services::cell::CellDataCache;
//
// #[test]
// fn test() {
// let mut ext = CellDataCache::new();
// ext.insert("1", "a".to_string());
// ext.insert("2", 2);
//
// let a: &String = ext.get("1").unwrap();
// assert_eq!(a, "a");
//
// let a: Option<&usize> = ext.get("1");
// assert!(a.is_none());
// }
// }

View File

@ -1,26 +1,10 @@
use crate::entities::FieldType;
use crate::services::cell::{CellProtobufBlob, TypeCellData};
use crate::services::cell::{AtomicCellDataCache, CellProtobufBlob, TypeCellData};
use crate::services::field::*;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use grid_rev_model::{CellRevision, FieldRevision};
use std::cmp::Ordering;
use std::fmt::Debug;
/// This trait is used when doing filter/search on the grid.
pub trait CellFilterable: TypeOptionConfiguration {
/// Return true if type_cell_data match the filter condition.
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool>;
}
pub trait CellComparable {
type CellData;
fn apply_cmp(&self, cell_data: &Self::CellData, other_cell_data: &Self::CellData) -> Ordering;
}
/// Decode the opaque cell data into readable format content
pub trait CellDataDecoder: TypeOption {
///
@ -57,7 +41,7 @@ pub trait CellDataChangeset: TypeOption {
&self,
changeset: <Self as TypeOption>::CellChangeset,
type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String>;
) -> FlowyResult<<Self as TypeOption>::CellData>;
}
/// changeset: It will be deserialized into specific data base on the FieldType.
@ -70,6 +54,7 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
changeset: C,
cell_rev: Option<CellRevision>,
field_rev: T,
cell_data_cache: Option<AtomicCellDataCache>,
) -> Result<String, FlowyError> {
let field_rev = field_rev.as_ref();
let changeset = changeset.to_string();
@ -80,9 +65,11 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
Err(_) => None,
});
let cell_data = match FieldRevisionExt::new(field_rev).get_type_option_cell_data_handler(&field_type) {
let cell_data = match TypeOptionCellExt::new_with_cell_data_cache(field_rev, cell_data_cache)
.get_type_option_cell_data_handler(&field_type)
{
None => "".to_string(),
Some(handler) => handler.handle_cell_changeset(changeset, type_cell_data)?,
Some(handler) => handler.handle_cell_changeset(changeset, type_cell_data, field_rev)?,
};
Ok(TypeCellData::new(cell_data, field_type).to_json())
}
@ -90,12 +77,13 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
pub fn decode_type_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debug>(
data: T,
field_rev: &FieldRevision,
cell_data_cache: Option<AtomicCellDataCache>,
) -> (FieldType, CellProtobufBlob) {
let to_field_type = field_rev.ty.into();
match data.try_into() {
Ok(type_cell_data) => {
let TypeCellData { cell_str, field_type } = type_cell_data;
match try_decode_cell_str(cell_str, &field_type, &to_field_type, field_rev) {
match try_decode_cell_str(cell_str, &field_type, &to_field_type, field_rev, cell_data_cache) {
Ok(cell_bytes) => (field_type, cell_bytes),
Err(e) => {
tracing::error!("Decode cell data failed, {:?}", e);
@ -134,8 +122,11 @@ pub fn try_decode_cell_str(
from_field_type: &FieldType,
to_field_type: &FieldType,
field_rev: &FieldRevision,
cell_data_cache: Option<AtomicCellDataCache>,
) -> FlowyResult<CellProtobufBlob> {
match FieldRevisionExt::new(field_rev).get_type_option_cell_data_handler(to_field_type) {
match TypeOptionCellExt::new_with_cell_data_cache(field_rev, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)
{
None => Ok(CellProtobufBlob::default()),
Some(handler) => handler.handle_cell_str(cell_str, from_field_type, field_rev),
}
@ -146,29 +137,30 @@ pub fn try_decode_cell_str(
/// empty string. For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma.
pub fn stringify_cell_data(
cell_data: String,
cell_str: String,
from_field_type: &FieldType,
to_field_type: &FieldType,
field_rev: &FieldRevision,
) -> String {
match FieldRevisionExt::new(field_rev).get_type_option_cell_data_handler(to_field_type) {
match TypeOptionCellExt::new_with_cell_data_cache(field_rev, None).get_type_option_cell_data_handler(to_field_type)
{
None => "".to_string(),
Some(handler) => handler.stringify_cell_str(cell_data, from_field_type, field_rev),
Some(handler) => handler.stringify_cell_str(cell_str, from_field_type, field_rev),
}
}
pub fn insert_text_cell(s: String, field_rev: &FieldRevision) -> CellRevision {
let data = apply_cell_data_changeset(s, None, field_rev).unwrap();
let data = apply_cell_data_changeset(s, None, field_rev, None).unwrap();
CellRevision::new(data)
}
pub fn insert_number_cell(num: i64, field_rev: &FieldRevision) -> CellRevision {
let data = apply_cell_data_changeset(num, None, field_rev).unwrap();
let data = apply_cell_data_changeset(num, None, field_rev, None).unwrap();
CellRevision::new(data)
}
pub fn insert_url_cell(url: String, field_rev: &FieldRevision) -> CellRevision {
let data = apply_cell_data_changeset(url, None, field_rev).unwrap();
let data = apply_cell_data_changeset(url, None, field_rev, None).unwrap();
CellRevision::new(data)
}
@ -178,7 +170,7 @@ pub fn insert_checkbox_cell(is_check: bool, field_rev: &FieldRevision) -> CellRe
} else {
UNCHECK.to_string()
};
let data = apply_cell_data_changeset(s, None, field_rev).unwrap();
let data = apply_cell_data_changeset(s, None, field_rev, None).unwrap();
CellRevision::new(data)
}
@ -189,19 +181,19 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi
is_utc: true,
})
.unwrap();
let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
let data = apply_cell_data_changeset(cell_data, None, field_rev, None).unwrap();
CellRevision::new(data)
}
pub fn insert_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
let data = apply_cell_data_changeset(cell_data, None, field_rev, None).unwrap();
CellRevision::new(data)
}
pub fn delete_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
let data = apply_cell_data_changeset(cell_data, None, field_rev, None).unwrap();
CellRevision::new(data)
}

View File

@ -1,5 +1,7 @@
mod cell_data_cache;
mod cell_operation;
mod type_cell_data;
pub use cell_data_cache::*;
pub use cell_operation::*;
pub use type_cell_data::*;

View File

@ -1,7 +1,5 @@
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB, TypeOptionCellData, TypeOptionConfiguration};
use flowy_error::FlowyResult;
use crate::services::field::CheckboxCellData;
impl CheckboxFilterPB {
pub fn is_visible(&self, cell_data: &CheckboxCellData) -> bool {
@ -13,20 +11,6 @@ impl CheckboxFilterPB {
}
}
impl CellFilterable for CheckboxTypeOptionPB {
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool> {
if !type_cell_data.is_checkbox() {
return Ok(true);
}
let checkbox_cell_data = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
Ok(filter.is_visible(&checkbox_cell_data))
}
}
#[cfg(test)]
mod tests {
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};

View File

@ -2,14 +2,15 @@ use crate::entities::{CheckboxFilterPB, FieldType};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, CellDataDecoder, FromCellString, TypeCellData};
use crate::services::field::{
BoxTypeOptionBuilder, CheckboxCellData, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration,
TypeOptionTransform,
default_order, BoxTypeOptionBuilder, CheckboxCellData, TypeOption, TypeOptionBuilder, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::FlowyResult;
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::str::FromStr;
#[derive(Default)]
@ -45,14 +46,11 @@ impl TypeOption for CheckboxTypeOptionPB {
type CellData = CheckboxCellData;
type CellChangeset = CheckboxCellChangeset;
type CellProtobufType = CheckboxCellData;
type CellFilter = CheckboxFilterPB;
}
impl TypeOptionTransform for CheckboxTypeOptionPB {}
impl TypeOptionConfiguration for CheckboxTypeOptionPB {
type CellFilterConfiguration = CheckboxFilterPB;
}
impl TypeOptionCellData for CheckboxTypeOptionPB {
fn convert_to_protobuf(&self, cell_data: <Self as TypeOption>::CellData) -> <Self as TypeOption>::CellProtobufType {
cell_data
@ -89,8 +87,37 @@ impl CellDataChangeset for CheckboxTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
let cell_data = CheckboxCellData::from_str(&changeset)?;
Ok(cell_data.to_string())
) -> FlowyResult<<Self as TypeOption>::CellData> {
let checkbox_cell_data = CheckboxCellData::from_str(&changeset)?;
Ok(checkbox_cell_data)
}
}
impl TypeOptionCellDataFilter for CheckboxTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_checkbox() {
return true;
}
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for CheckboxTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
match (cell_data.is_check(), other_cell_data.is_check()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => default_order(),
}
}
}

View File

@ -7,7 +7,7 @@ use std::str::FromStr;
pub const CHECK: &str = "Yes";
pub const UNCHECK: &str = "No";
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub struct CheckboxCellData(String);
impl CheckboxCellData {

View File

@ -1,9 +1,6 @@
use crate::entities::{DateFilterConditionPB, DateFilterPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{DateTypeOptionPB, TypeOptionCellData, TypeOptionConfiguration};
use chrono::NaiveDateTime;
use flowy_error::FlowyResult;
use chrono::NaiveDateTime;
impl DateFilterPB {
pub fn is_visible<T: Into<Option<i64>>>(&self, cell_timestamp: T) -> bool {
@ -60,21 +57,6 @@ impl DateFilterPB {
}
}
impl CellFilterable for DateTypeOptionPB {
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool> {
if !type_cell_data.is_date() {
return Ok(true);
}
let date_cell_data = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
Ok(filter.is_visible(date_cell_data))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]

View File

@ -155,7 +155,7 @@ mod tests {
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
decode_cell_data(encoded_data, type_option, field_rev),
decode_cell_data(encoded_data.to_string(), type_option, field_rev),
expected_str.to_owned(),
);
}

View File

@ -2,8 +2,9 @@ use crate::entities::{DateFilterPB, FieldType};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, CellDataDecoder, FromCellString, TypeCellData};
use crate::services::field::{
BoxTypeOptionBuilder, DateCellChangeset, DateCellData, DateCellDataPB, DateFormat, TimeFormat, TypeOption,
TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration, TypeOptionTransform,
default_order, BoxTypeOptionBuilder, DateCellChangeset, DateCellData, DateCellDataPB, DateFormat, TimeFormat,
TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionTransform,
};
use bytes::Bytes;
use chrono::format::strftime::StrftimeItems;
@ -12,6 +13,7 @@ use flowy_derive::ProtoBuf;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
// Date
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
@ -31,10 +33,7 @@ impl TypeOption for DateTypeOptionPB {
type CellData = DateCellData;
type CellChangeset = DateCellChangeset;
type CellProtobufType = DateCellDataPB;
}
impl TypeOptionConfiguration for DateTypeOptionPB {
type CellFilterConfiguration = DateFilterPB;
type CellFilter = DateFilterPB;
}
impl TypeOptionCellData for DateTypeOptionPB {
@ -158,7 +157,7 @@ impl CellDataChangeset for DateTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let cell_data = match changeset.date_timestamp() {
None => 0,
Some(date_timestamp) => match (self.include_time, changeset.time) {
@ -173,7 +172,37 @@ impl CellDataChangeset for DateTypeOptionPB {
},
};
Ok(cell_data.to_string())
Ok(DateCellData(Some(cell_data)))
}
}
impl TypeOptionCellDataFilter for DateTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_date() {
return true;
}
filter.is_visible(cell_data.0)
}
}
impl TypeOptionCellDataCompare for DateTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
match (cell_data.0, other_cell_data.0) {
(Some(left), Some(right)) => left.cmp(&right),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => default_order(),
}
}
}

View File

@ -68,7 +68,7 @@ impl ToString for DateCellChangeset {
}
}
#[derive(Default)]
#[derive(Default, Clone)]
pub struct DateCellData(pub Option<i64>);
impl std::convert::From<DateCellData> for i64 {
@ -93,6 +93,15 @@ impl FromCellString for DateCellData {
}
}
impl ToString for DateCellData {
fn to_string(&self) -> String {
match self.0 {
None => "".to_string(),
Some(val) => val.to_string(),
}
}
}
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum DateFormat {
Local = 0,

View File

@ -4,6 +4,7 @@ pub mod number_type_option;
pub mod selection_type_option;
pub mod text_type_option;
mod type_option;
mod type_option_cell;
pub mod url_type_option;
pub use checkbox_type_option::*;
@ -12,4 +13,5 @@ pub use number_type_option::*;
pub use selection_type_option::*;
pub use text_type_option::*;
pub use type_option::*;
pub use type_option_cell::*;
pub use url_type_option::*;

View File

@ -1,7 +1,7 @@
use crate::entities::{NumberFilterConditionPB, NumberFilterPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{NumberCellData, NumberTypeOptionPB, TypeOptionConfiguration};
use flowy_error::FlowyResult;
use crate::services::field::NumberCellData;
use rust_decimal::prelude::Zero;
use rust_decimal::Decimal;
use std::str::FromStr;
@ -37,23 +37,6 @@ impl NumberFilterPB {
}
}
impl CellFilterable for NumberTypeOptionPB {
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool> {
if !type_cell_data.is_number() {
return Ok(true);
}
let cell_data = type_cell_data.cell_str;
let num_cell_data = self.format_cell_data(&cell_data)?;
Ok(filter.is_visible(&num_cell_data))
}
}
#[cfg(test)]
mod tests {
use crate::entities::{NumberFilterConditionPB, NumberFilterPB};

View File

@ -1,10 +1,10 @@
use crate::entities::{FieldType, NumberFilterPB};
use crate::impl_type_option;
use crate::services::cell::{CellComparable, CellDataChangeset, CellDataDecoder, TypeCellData};
use crate::services::cell::{CellDataChangeset, CellDataDecoder, TypeCellData};
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{
BoxTypeOptionBuilder, NumberCellData, StrCellData, TypeOption, TypeOptionBuilder, TypeOptionCellData,
TypeOptionConfiguration, TypeOptionTransform,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
@ -77,10 +77,7 @@ impl TypeOption for NumberTypeOptionPB {
type CellData = StrCellData;
type CellChangeset = NumberCellChangeset;
type CellProtobufType = StrCellData;
}
impl TypeOptionConfiguration for NumberTypeOptionPB {
type CellFilterConfiguration = NumberFilterPB;
type CellFilter = NumberFilterPB;
}
impl TypeOptionCellData for NumberTypeOptionPB {
@ -158,20 +155,39 @@ impl CellDataChangeset for NumberTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let data = changeset.trim().to_string();
let _ = self.format_cell_data(&data)?;
Ok(data)
}
}
impl CellComparable for NumberTypeOptionPB {
type CellData = NumberCellData;
fn apply_cmp(&self, _cell_data: &Self::CellData, _other_cell_data: &Self::CellData) -> Ordering {
Ordering::Equal
Ok(StrCellData(data))
}
}
impl TypeOptionCellDataFilter for NumberTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_number() {
return true;
}
match self.format_cell_data(cell_data) {
Ok(cell_data) => filter.is_visible(&cell_data),
Err(_) => true,
}
}
}
impl TypeOptionCellDataCompare for NumberTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.0.cmp(&other_cell_data.0)
}
}
impl std::default::Default for NumberTypeOptionPB {
fn default() -> Self {
let format = NumberFormat::default();

View File

@ -1,16 +1,17 @@
use crate::entities::{ChecklistFilterPB, FieldType};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, FromCellString, TypeCellData};
use crate::services::field::{
BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB, SelectOptionIds, SelectOptionPB,
SelectTypeOptionSharedAction, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration,
SelectTypeOptionSharedAction, SelectedSelectOptions, TypeOption, TypeOptionBuilder, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::FlowyResult;
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
// Multiple select
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
@ -27,10 +28,7 @@ impl TypeOption for ChecklistTypeOptionPB {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
}
impl TypeOptionConfiguration for ChecklistTypeOptionPB {
type CellFilterConfiguration = ChecklistFilterPB;
type CellFilter = ChecklistFilterPB;
}
impl TypeOptionCellData for ChecklistTypeOptionPB {
@ -62,7 +60,7 @@ impl CellDataChangeset for ChecklistTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let insert_option_ids = changeset
.insert_option_ids
.into_iter()
@ -70,7 +68,7 @@ impl CellDataChangeset for ChecklistTypeOptionPB {
.collect::<Vec<String>>();
match type_cell_data {
None => Ok(SelectOptionIds::from(insert_option_ids).to_string()),
None => Ok(SelectOptionIds::from(insert_option_ids)),
Some(type_cell_data) => {
let mut select_ids: SelectOptionIds = type_cell_data.cell_str.into();
for insert_option_id in insert_option_ids {
@ -83,11 +81,35 @@ impl CellDataChangeset for ChecklistTypeOptionPB {
select_ids.retain(|id| id != &delete_option_id);
}
Ok(select_ids.to_string())
Ok(select_ids)
}
}
}
}
impl TypeOptionCellDataFilter for ChecklistTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_check_list() {
return true;
}
let selected_options = SelectedSelectOptions::from(self.get_selected_options(cell_data.clone()));
filter.is_visible(&self.options, &selected_options)
}
}
impl TypeOptionCellDataCompare for ChecklistTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.len().cmp(&other_cell_data.len())
}
}
#[derive(Default)]
pub struct ChecklistTypeOptionBuilder(ChecklistTypeOptionPB);

View File

@ -1,10 +1,12 @@
use crate::entities::{FieldType, SelectOptionFilterPB};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, FromCellString, TypeCellData};
use std::cmp::{min, Ordering};
use crate::services::field::{
BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB, SelectOptionIds, SelectOptionPB,
SelectTypeOptionSharedAction, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration,
default_order, BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB, SelectOptionIds,
SelectOptionPB, SelectTypeOptionSharedAction, SelectedSelectOptions, TypeOption, TypeOptionBuilder,
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
@ -27,10 +29,7 @@ impl TypeOption for MultiSelectTypeOptionPB {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
}
impl TypeOptionConfiguration for MultiSelectTypeOptionPB {
type CellFilterConfiguration = SelectOptionFilterPB;
type CellFilter = SelectOptionFilterPB;
}
impl TypeOptionCellData for MultiSelectTypeOptionPB {
@ -62,18 +61,15 @@ impl CellDataChangeset for MultiSelectTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let insert_option_ids = changeset
.insert_option_ids
.into_iter()
.filter(|insert_option_id| self.options.iter().any(|option| &option.id == insert_option_id))
.collect::<Vec<String>>();
let new_cell_data: String;
match type_cell_data {
None => {
new_cell_data = SelectOptionIds::from(insert_option_ids).to_string();
}
None => Ok(SelectOptionIds::from(insert_option_ids)),
Some(type_cell_data) => {
let mut select_ids: SelectOptionIds = type_cell_data.cell_str.into();
for insert_option_id in insert_option_ids {
@ -86,15 +82,56 @@ impl CellDataChangeset for MultiSelectTypeOptionPB {
select_ids.retain(|id| id != &delete_option_id);
}
new_cell_data = select_ids.to_string();
tracing::trace!("Multi-select cell data: {}", &new_cell_data);
tracing::trace!("Multi-select cell data: {}", select_ids.to_string());
Ok(select_ids)
}
}
Ok(new_cell_data)
}
}
impl TypeOptionCellDataFilter for MultiSelectTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_multi_select() {
return true;
}
let selected_options = SelectedSelectOptions::from(self.get_selected_options(cell_data.clone()));
filter.is_visible(&selected_options, FieldType::MultiSelect)
}
}
impl TypeOptionCellDataCompare for MultiSelectTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
for i in 0..min(cell_data.len(), other_cell_data.len()) {
let order = match (
cell_data
.get(i)
.and_then(|id| self.options.iter().find(|option| &option.id == id)),
other_cell_data
.get(i)
.and_then(|id| self.options.iter().find(|option| &option.id == id)),
) {
(Some(left), Some(right)) => left.name.cmp(&right.name),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => default_order(),
};
if order.is_ne() {
return order;
}
}
default_order()
}
}
#[derive(Default)]
pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOptionPB);
impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder);
@ -173,7 +210,7 @@ mod tests {
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id, facebook.id];
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(&*select_option_ids, &option_ids);
}
@ -192,12 +229,12 @@ mod tests {
// insert
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(&*select_option_ids, &option_ids);
// delete
let changeset = SelectOptionCellChangeset::from_delete_options(option_ids);
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
@ -213,8 +250,8 @@ mod tests {
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id);
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(cell_option_ids, google.id);
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(select_option_ids.to_string(), google.id);
}
#[test]
@ -228,8 +265,8 @@ mod tests {
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id);
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(cell_option_ids.is_empty());
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
#[test]
@ -246,11 +283,11 @@ mod tests {
// empty option id string
let changeset = SelectOptionCellChangeset::from_insert_option_id("");
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(cell_option_ids, "");
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(select_option_ids.to_string(), "");
let changeset = SelectOptionCellChangeset::from_insert_option_id("123,456");
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(cell_option_ids, "");
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
}

View File

@ -1,12 +1,7 @@
#![allow(clippy::needless_collect)]
use crate::entities::{ChecklistFilterPB, FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{
ChecklistTypeOptionPB, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB, TypeOptionCellData,
};
use crate::services::field::{SelectTypeOptionSharedAction, SelectedSelectOptions};
use flowy_error::FlowyResult;
use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use crate::services::field::SelectedSelectOptions;
impl SelectOptionFilterPB {
pub fn is_visible(&self, selected_options: &SelectedSelectOptions, field_type: FieldType) -> bool {
@ -80,39 +75,6 @@ impl SelectOptionFilterPB {
}
}
impl CellFilterable for MultiSelectTypeOptionPB {
fn apply_filter(&self, type_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
if !type_cell_data.is_multi_select() {
return Ok(true);
}
let ids = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
let selected_options = SelectedSelectOptions::from(self.get_selected_options(ids));
Ok(filter.is_visible(&selected_options, FieldType::MultiSelect))
}
}
impl CellFilterable for SingleSelectTypeOptionPB {
fn apply_filter(&self, type_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
if !type_cell_data.is_single_select() {
return Ok(true);
}
let ids = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
let selected_options = SelectedSelectOptions::from(self.get_selected_options(ids));
Ok(filter.is_visible(&selected_options, FieldType::SingleSelect))
}
}
impl CellFilterable for ChecklistTypeOptionPB {
fn apply_filter(&self, type_cell_data: TypeCellData, filter: &ChecklistFilterPB) -> FlowyResult<bool> {
if !type_cell_data.is_checklist() {
return Ok(true);
}
let ids = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
let selected_options = SelectedSelectOptions::from(self.get_selected_options(ids));
Ok(filter.is_visible(&self.options, &selected_options))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]

View File

@ -252,7 +252,7 @@ pub fn new_select_option_color(options: &Vec<SelectOptionPB>) -> SelectOptionCol
/// Calls [to_string] will return a string consists list of ids,
/// placing a commas separator between each
///
#[derive(Default)]
#[derive(Default, Clone)]
pub struct SelectOptionIds(Vec<String>);
impl SelectOptionIds {

View File

@ -1,10 +1,11 @@
use crate::entities::{FieldType, SelectOptionFilterPB};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, FromCellString, TypeCellData};
use std::cmp::Ordering;
use crate::services::field::{
BoxTypeOptionBuilder, SelectOptionCellDataPB, TypeOption, TypeOptionBuilder, TypeOptionCellData,
TypeOptionConfiguration,
default_order, BoxTypeOptionBuilder, SelectOptionCellDataPB, SelectedSelectOptions, TypeOption, TypeOptionBuilder,
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
};
use crate::services::field::{
SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
@ -30,10 +31,7 @@ impl TypeOption for SingleSelectTypeOptionPB {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
}
impl TypeOptionConfiguration for SingleSelectTypeOptionPB {
type CellFilterConfiguration = SelectOptionFilterPB;
type CellFilter = SelectOptionFilterPB;
}
impl TypeOptionCellData for SingleSelectTypeOptionPB {
@ -65,7 +63,7 @@ impl CellDataChangeset for SingleSelectTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let mut insert_option_ids = changeset
.insert_option_ids
.into_iter()
@ -76,15 +74,51 @@ impl CellDataChangeset for SingleSelectTypeOptionPB {
// Sometimes, the insert_option_ids may contain list of option ids. For example,
// copy/paste a ids string.
if insert_option_ids.is_empty() {
Ok("".to_string())
Ok(SelectOptionIds::from(insert_option_ids))
} else {
// Just take the first select option
let _ = insert_option_ids.drain(1..);
Ok(insert_option_ids.pop().unwrap())
Ok(SelectOptionIds::from(insert_option_ids))
}
}
}
impl TypeOptionCellDataFilter for SingleSelectTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_single_select() {
return true;
}
let selected_options = SelectedSelectOptions::from(self.get_selected_options(cell_data.clone()));
filter.is_visible(&selected_options, FieldType::SingleSelect)
}
}
impl TypeOptionCellDataCompare for SingleSelectTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
match (
cell_data
.first()
.and_then(|id| self.options.iter().find(|option| &option.id == id)),
other_cell_data
.first()
.and_then(|id| self.options.iter().find(|option| &option.id == id)),
) {
(Some(left), Some(right)) => left.name.cmp(&right.name),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => default_order(),
}
}
}
#[derive(Default)]
pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOptionPB);
impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder);
@ -161,8 +195,7 @@ mod tests {
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id.clone(), facebook.id];
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids);
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(&*select_option_ids, &vec![google.id]);
}
@ -180,12 +213,12 @@ mod tests {
// insert
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(&*select_option_ids, &vec![google.id]);
// delete
let changeset = SelectOptionCellChangeset::from_delete_options(option_ids);
let select_option_ids: SelectOptionIds = type_option.apply_changeset(changeset, None).unwrap().into();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
@ -198,9 +231,9 @@ mod tests {
let option_ids = vec![google.id];
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids);
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(cell_option_ids.is_empty());
assert!(select_option_ids.is_empty());
}
#[test]
@ -210,7 +243,7 @@ mod tests {
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
let changeset = SelectOptionCellChangeset::from_insert_option_id("");
let cell_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(cell_option_ids, "");
let select_option_ids = type_option.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
}

View File

@ -1,7 +1,4 @@
use crate::entities::{TextFilterConditionPB, TextFilterPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{RichTextTypeOptionPB, TypeOptionCellData, TypeOptionConfiguration};
use flowy_error::FlowyResult;
impl TextFilterPB {
pub fn is_visible<T: AsRef<str>>(&self, cell_data: T) -> bool {
@ -20,21 +17,6 @@ impl TextFilterPB {
}
}
impl CellFilterable for RichTextTypeOptionPB {
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool> {
if !type_cell_data.is_text() {
return Ok(false);
}
let text_cell_data = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
Ok(filter.is_visible(text_cell_data))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]

View File

@ -52,28 +52,28 @@ mod tests {
let text_type_option = RichTextTypeOptionPB::default();
let field_type = FieldType::MultiSelect;
let France = SelectOptionPB::new("France");
let france_optionId = France.id.clone();
let france = SelectOptionPB::new("france");
let france_option_id = france.id.clone();
let Argentina = SelectOptionPB::new("Argentina");
let argentina_optionId = Argentina.id.clone();
let argentina = SelectOptionPB::new("argentina");
let argentina_option_id = argentina.id.clone();
let multi_select = MultiSelectTypeOptionBuilder::default()
.add_option(France.clone())
.add_option(Argentina.clone());
.add_option(france.clone())
.add_option(argentina.clone());
let field_rev = FieldBuilder::new(multi_select).build();
assert_eq!(
text_type_option
.decode_cell_str(
format!("{},{}", france_optionId, argentina_optionId),
format!("{},{}", france_option_id, argentina_option_id),
&field_type,
&field_rev
)
.unwrap()
.to_string(),
format!("{},{}", France.name, Argentina.name)
format!("{},{}", france.name, argentina.name)
);
}
}

View File

@ -1,12 +1,12 @@
use crate::entities::{FieldType, TextFilterPB};
use crate::impl_type_option;
use crate::services::cell::{
stringify_cell_data, CellComparable, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser, DecodedCellData,
FromCellString, TypeCellData,
stringify_cell_data, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellString,
TypeCellData,
};
use crate::services::field::{
BoxTypeOptionBuilder, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration,
TypeOptionTransform,
BoxTypeOptionBuilder, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
@ -45,14 +45,11 @@ impl TypeOption for RichTextTypeOptionPB {
type CellData = StrCellData;
type CellChangeset = String;
type CellProtobufType = StrCellData;
type CellFilter = TextFilterPB;
}
impl TypeOptionTransform for RichTextTypeOptionPB {}
impl TypeOptionConfiguration for RichTextTypeOptionPB {
type CellFilterConfiguration = TextFilterPB;
}
impl TypeOptionCellData for RichTextTypeOptionPB {
fn convert_to_protobuf(&self, cell_data: <Self as TypeOption>::CellData) -> <Self as TypeOption>::CellProtobufType {
cell_data
@ -92,23 +89,41 @@ impl CellDataChangeset for RichTextTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
if changeset.len() > 10000 {
Err(FlowyError::text_too_long().context("The len of the text should not be more than 10000"))
} else {
Ok(changeset)
Ok(StrCellData(changeset))
}
}
}
impl CellComparable for RichTextTypeOptionPB {
type CellData = String;
impl TypeOptionCellDataFilter for RichTextTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_text() {
return false;
}
fn apply_cmp(&self, cell_data: &Self::CellData, other_cell_data: &Self::CellData) -> Ordering {
cell_data.cmp(other_cell_data)
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for RichTextTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.0.cmp(&other_cell_data.0)
}
}
#[derive(Clone)]
pub struct TextCellData(pub String);
impl AsRef<str> for TextCellData {
fn as_ref(&self) -> &str {
@ -158,7 +173,7 @@ impl CellProtobufBlobParser for TextCellDataParser {
}
}
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub struct StrCellData(pub String);
impl std::ops::Deref for StrCellData {
type Target = String;

View File

@ -1,15 +1,12 @@
use crate::entities::FieldType;
use crate::services::cell::{
CellDataChangeset, CellDataDecoder, CellProtobufBlob, FromCellChangeset, FromCellString, TypeCellData,
};
use crate::services::field::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, MultiSelectTypeOptionPB, NumberTypeOptionPB,
RichTextTypeOptionPB, SingleSelectTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::{CellDataDecoder, FromCellChangeset, FromCellString};
use crate::services::filter::FromFilterString;
use bytes::Bytes;
use flowy_error::FlowyResult;
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use grid_rev_model::FieldRevision;
use protobuf::ProtobufError;
use std::cmp::Ordering;
use std::fmt::Debug;
pub trait TypeOption {
@ -23,7 +20,7 @@ pub trait TypeOption {
///
/// Uses `StrCellData` for any `TypeOption` if their cell data is pure `String`.
///
type CellData: FromCellString + Default;
type CellData: FromCellString + ToString + Default + Send + Sync + Clone + 'static;
/// Represents as the corresponding field type cell changeset.
/// The changeset must implements the `FromCellChangeset` trait. The `CellChangeset` is implemented
@ -39,6 +36,9 @@ pub trait TypeOption {
/// FieldType::URL => URLCellDataPB
///
type CellProtobufType: TryInto<Bytes, Error = ProtobufError> + Debug;
/// Represents as the filter configuration for this type option.
type CellFilter: FromFilterString + Send + Sync + 'static;
}
pub trait TypeOptionCellData: TypeOption {
@ -55,10 +55,6 @@ pub trait TypeOptionCellData: TypeOption {
fn decode_type_option_cell_str(&self, cell_str: String) -> FlowyResult<<Self as TypeOption>::CellData>;
}
pub trait TypeOptionConfiguration {
type CellFilterConfiguration;
}
pub trait TypeOptionTransform: TypeOption {
/// Returns true if the current `TypeOption` provides custom type option transformation
fn transformable(&self) -> bool {
@ -97,189 +93,24 @@ pub trait TypeOptionTransform: TypeOption {
}
}
/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait.
pub trait TypeOptionTransformHandler {
fn transform(&mut self, old_type_option_field_type: FieldType, old_type_option_data: String);
fn json_str(&self) -> String;
}
impl<T> TypeOptionTransformHandler for T
where
T: TypeOptionTransform + TypeOptionDataSerializer,
{
fn transform(&mut self, old_type_option_field_type: FieldType, old_type_option_data: String) {
if self.transformable() {
self.transform_type_option(old_type_option_field_type, old_type_option_data)
}
}
fn json_str(&self) -> String {
self.json_str()
}
}
/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait
/// Only object-safe traits can be made into trait objects.
/// > Object-safe traits are traits with methods that follow these two rules:
/// 1.the return type is not Self.
/// 2.there are no generic types parameters.
///
pub trait TypeOptionCellDataHandler {
fn handle_cell_str(
&self,
cell_str: String,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellProtobufBlob>;
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String>;
/// Decode the cell_str to corresponding cell data, and then return the display string of the
/// cell data.
fn stringify_cell_str(&self, cell_str: String, field_type: &FieldType, field_rev: &FieldRevision) -> String;
}
impl<T> TypeOptionCellDataHandler for T
where
T: TypeOption + CellDataDecoder + CellDataChangeset + TypeOptionCellData + TypeOptionTransform,
{
fn handle_cell_str(
&self,
cell_str: String,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellProtobufBlob> {
let cell_data = if self.transformable() {
match self.transform_type_option_cell_str(&cell_str, decoded_field_type, field_rev) {
None => self.decode_cell_str(cell_str, decoded_field_type, field_rev)?,
Some(cell_data) => cell_data,
}
} else {
self.decode_cell_str(cell_str, decoded_field_type, field_rev)?
};
CellProtobufBlob::from(self.convert_to_protobuf(cell_data))
}
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
let changeset = <Self as TypeOption>::CellChangeset::from_changeset(cell_changeset)?;
let cell_data = self.apply_changeset(changeset, old_type_cell_data)?;
Ok(cell_data)
}
fn stringify_cell_str(&self, cell_str: String, field_type: &FieldType, field_rev: &FieldRevision) -> String {
if self.transformable() {
let cell_data = self.transform_type_option_cell_str(&cell_str, field_type, field_rev);
if let Some(cell_data) = cell_data {
return self.decode_cell_data_to_str(cell_data);
}
}
match <Self as TypeOption>::CellData::from_cell_str(&cell_str) {
Ok(cell_data) => self.decode_cell_data_to_str(cell_data),
Err(_) => "".to_string(),
}
}
}
pub struct FieldRevisionExt<'a> {
field_rev: &'a FieldRevision,
}
impl<'a> FieldRevisionExt<'a> {
pub fn new(field_rev: &'a FieldRevision) -> Self {
Self { field_rev }
}
pub fn get_type_option_cell_data_handler(
pub trait TypeOptionCellDataFilter: TypeOption + CellDataDecoder {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> {
match field_type {
FieldType::RichText => self
.field_rev
.get_type_option::<RichTextTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::Number => self
.field_rev
.get_type_option::<NumberTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::DateTime => self
.field_rev
.get_type_option::<DateTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::SingleSelect => self
.field_rev
.get_type_option::<SingleSelectTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::MultiSelect => self
.field_rev
.get_type_option::<MultiSelectTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::Checkbox => self
.field_rev
.get_type_option::<CheckboxTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::URL => self
.field_rev
.get_type_option::<URLTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
FieldType::Checklist => self
.field_rev
.get_type_option::<ChecklistTypeOptionPB>(field_type.into())
.map(|type_option| Box::new(type_option) as Box<dyn TypeOptionCellDataHandler>),
}
}
cell_data: &<Self as TypeOption>::CellData,
) -> bool;
}
pub fn transform_type_option(
type_option_data: &str,
new_field_type: &FieldType,
old_type_option_data: Option<String>,
old_field_type: FieldType,
) -> String {
let mut transform_handler = get_type_option_transform_handler(type_option_data, new_field_type);
if let Some(old_type_option_data) = old_type_option_data {
transform_handler.transform(old_field_type, old_type_option_data);
}
transform_handler.json_str()
#[inline(always)]
pub fn default_order() -> Ordering {
Ordering::Equal
}
pub fn get_type_option_transform_handler(
type_option_data: &str,
field_type: &FieldType,
) -> Box<dyn TypeOptionTransformHandler> {
match field_type {
FieldType::RichText => {
Box::new(RichTextTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Number => {
Box::new(NumberTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::DateTime => {
Box::new(DateTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::SingleSelect => {
Box::new(SingleSelectTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::MultiSelect => {
Box::new(MultiSelectTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Checkbox => {
Box::new(CheckboxTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::URL => {
Box::new(URLTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Checklist => {
Box::new(ChecklistTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
}
pub trait TypeOptionCellDataCompare: TypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering;
}

View File

@ -0,0 +1,420 @@
use crate::entities::FieldType;
use crate::services::cell::{
AtomicCellDataCache, AtomicCellFilterCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob,
FromCellChangeset, FromCellString, TypeCellData,
};
use crate::services::field::{
default_order, CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, MultiSelectTypeOptionPB,
NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOptionPB,
};
use crate::services::filter::FilterType;
use flowy_error::FlowyResult;
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait
/// Only object-safe traits can be made into trait objects.
/// > Object-safe traits are traits with methods that follow these two rules:
/// 1.the return type is not Self.
/// 2.there are no generic types parameters.
///
pub trait TypeOptionCellDataHandler {
fn handle_cell_str(
&self,
cell_str: String,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellProtobufBlob>;
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_type_cell_data: Option<TypeCellData>,
field_rev: &FieldRevision,
) -> FlowyResult<String>;
fn handle_cell_compare(&self, left_cell_data: &str, right_cell_data: &str, field_rev: &FieldRevision) -> Ordering;
fn handle_cell_filter(
&self,
filter_type: &FilterType,
field_rev: &FieldRevision,
type_cell_data: TypeCellData,
) -> bool;
/// Decode the cell_str to corresponding cell data, and then return the display string of the
/// cell data.
fn stringify_cell_str(&self, cell_str: String, field_type: &FieldType, field_rev: &FieldRevision) -> String;
}
struct CellDataCacheKey(u64);
impl CellDataCacheKey {
pub fn new(field_rev: &FieldRevision, decoded_field_type: FieldType, cell_str: &str) -> Self {
let mut hasher = DefaultHasher::new();
hasher.write(field_rev.id.as_bytes());
hasher.write_u8(decoded_field_type as u8);
hasher.write(cell_str.as_bytes());
Self(hasher.finish())
}
}
impl AsRef<u64> for CellDataCacheKey {
fn as_ref(&self) -> &u64 {
&self.0
}
}
struct TypeOptionCellDataHandlerImpl<T> {
inner: T,
cell_data_cache: Option<AtomicCellDataCache>,
cell_filter_cache: Option<AtomicCellFilterCache>,
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellData
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ 'static,
{
pub fn new_with_boxed(
inner: T,
cell_filter_cache: Option<AtomicCellFilterCache>,
cell_data_cache: Option<AtomicCellDataCache>,
) -> Box<dyn TypeOptionCellDataHandler> {
Box::new(Self {
inner,
cell_data_cache,
cell_filter_cache,
}) as Box<dyn TypeOptionCellDataHandler>
}
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption + CellDataDecoder,
{
fn get_decoded_cell_data(
&self,
cell_str: String,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<<Self as TypeOption>::CellData> {
let key = CellDataCacheKey::new(field_rev, decoded_field_type.clone(), &cell_str);
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let read_guard = cell_data_cache.read();
if let Some(cell_data) = read_guard.get(key.as_ref()).cloned() {
tracing::trace!("Cell cache hit: {}:{}", decoded_field_type, cell_str);
return Ok(cell_data);
}
}
let cell_data = self.decode_cell_str(cell_str, decoded_field_type, field_rev)?;
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
cell_data_cache.write().insert(key.as_ref(), cell_data.clone());
}
Ok(cell_data)
}
fn set_decoded_cell_data(&self, cell_data: <Self as TypeOption>::CellData, field_rev: &FieldRevision) {
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type: FieldType = field_rev.ty.into();
let cell_str = cell_data.to_string();
tracing::trace!("Update cell cache {}:{}", field_type, cell_str);
let key = CellDataCacheKey::new(field_rev, field_type, &cell_str);
cell_data_cache.write().insert(key.as_ref(), cell_data);
}
}
}
impl<T> std::ops::Deref for TypeOptionCellDataHandlerImpl<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> TypeOption for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption,
{
type CellData = T::CellData;
type CellChangeset = T::CellChangeset;
type CellProtobufType = T::CellProtobufType;
type CellFilter = T::CellFilter;
}
impl<T> TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellData
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare,
{
fn handle_cell_str(
&self,
cell_str: String,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellProtobufBlob> {
let cell_data = if self.transformable() {
match self.transform_type_option_cell_str(&cell_str, decoded_field_type, field_rev) {
None => self.get_decoded_cell_data(cell_str, decoded_field_type, field_rev)?,
Some(cell_data) => cell_data,
}
} else {
self.get_decoded_cell_data(cell_str, decoded_field_type, field_rev)?
};
CellProtobufBlob::from(self.convert_to_protobuf(cell_data))
}
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_type_cell_data: Option<TypeCellData>,
field_rev: &FieldRevision,
) -> FlowyResult<String> {
let changeset = <Self as TypeOption>::CellChangeset::from_changeset(cell_changeset)?;
let cell_data = self.apply_changeset(changeset, old_type_cell_data)?;
self.set_decoded_cell_data(cell_data.clone(), field_rev);
Ok(cell_data.to_string())
}
fn handle_cell_compare(&self, left_cell_data: &str, right_cell_data: &str, field_rev: &FieldRevision) -> Ordering {
let field_type: FieldType = field_rev.ty.into();
let left = self.get_decoded_cell_data(left_cell_data.to_owned(), &field_type, field_rev);
let right = self.get_decoded_cell_data(right_cell_data.to_owned(), &field_type, field_rev);
match (left, right) {
(Ok(left), Ok(right)) => self.apply_cmp(&left, &right),
(Ok(_), Err(_)) => Ordering::Greater,
(Err(_), Ok(_)) => Ordering::Less,
(Err(_), Err(_)) => default_order(),
}
}
fn handle_cell_filter(
&self,
filter_type: &FilterType,
field_rev: &FieldRevision,
type_cell_data: TypeCellData,
) -> bool {
let perform_filter = || {
let filter_cache = self.cell_filter_cache.as_ref()?.read();
let cell_filter = filter_cache.get::<<Self as TypeOption>::CellFilter>(filter_type)?;
let cell_data = self
.get_decoded_cell_data(type_cell_data.cell_str, &filter_type.field_type, field_rev)
.ok()?;
Some(self.apply_filter(cell_filter, &filter_type.field_type, &cell_data))
};
perform_filter().unwrap_or(true)
}
fn stringify_cell_str(&self, cell_str: String, field_type: &FieldType, field_rev: &FieldRevision) -> String {
if self.transformable() {
let cell_data = self.transform_type_option_cell_str(&cell_str, field_type, field_rev);
if let Some(cell_data) = cell_data {
return self.decode_cell_data_to_str(cell_data);
}
}
match <Self as TypeOption>::CellData::from_cell_str(&cell_str) {
Ok(cell_data) => self.decode_cell_data_to_str(cell_data),
Err(_) => "".to_string(),
}
}
}
pub struct TypeOptionCellExt<'a> {
field_rev: &'a FieldRevision,
cell_data_cache: Option<AtomicCellDataCache>,
cell_filter_cache: Option<AtomicCellFilterCache>,
}
impl<'a> TypeOptionCellExt<'a> {
pub fn new_with_cell_data_cache(
field_rev: &'a FieldRevision,
cell_data_cache: Option<AtomicCellDataCache>,
) -> Self {
Self {
field_rev,
cell_data_cache,
cell_filter_cache: None,
}
}
pub fn new(
field_rev: &'a FieldRevision,
cell_data_cache: Option<AtomicCellDataCache>,
cell_filter_cache: Option<AtomicCellFilterCache>,
) -> Self {
let mut this = Self::new_with_cell_data_cache(field_rev, cell_data_cache);
this.cell_filter_cache = cell_filter_cache;
this
}
pub fn get_type_option_cell_data_handler(
&self,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> {
match field_type {
FieldType::RichText => self
.field_rev
.get_type_option::<RichTextTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::Number => self
.field_rev
.get_type_option::<NumberTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::DateTime => self
.field_rev
.get_type_option::<DateTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::SingleSelect => self
.field_rev
.get_type_option::<SingleSelectTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::MultiSelect => self
.field_rev
.get_type_option::<MultiSelectTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::Checkbox => self
.field_rev
.get_type_option::<CheckboxTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::URL => self
.field_rev
.get_type_option::<URLTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::Checklist => self
.field_rev
.get_type_option::<ChecklistTypeOptionPB>(field_type.into())
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
}
}
}
pub fn transform_type_option(
type_option_data: &str,
new_field_type: &FieldType,
old_type_option_data: Option<String>,
old_field_type: FieldType,
) -> String {
let mut transform_handler = get_type_option_transform_handler(type_option_data, new_field_type);
if let Some(old_type_option_data) = old_type_option_data {
transform_handler.transform(old_field_type, old_type_option_data);
}
transform_handler.json_str()
}
/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait.
pub trait TypeOptionTransformHandler {
fn transform(&mut self, old_type_option_field_type: FieldType, old_type_option_data: String);
fn json_str(&self) -> String;
}
impl<T> TypeOptionTransformHandler for T
where
T: TypeOptionTransform + TypeOptionDataSerializer,
{
fn transform(&mut self, old_type_option_field_type: FieldType, old_type_option_data: String) {
if self.transformable() {
self.transform_type_option(old_type_option_field_type, old_type_option_data)
}
}
fn json_str(&self) -> String {
self.json_str()
}
}
fn get_type_option_transform_handler(
type_option_data: &str,
field_type: &FieldType,
) -> Box<dyn TypeOptionTransformHandler> {
match field_type {
FieldType::RichText => {
Box::new(RichTextTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Number => {
Box::new(NumberTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::DateTime => {
Box::new(DateTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::SingleSelect => {
Box::new(SingleSelectTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::MultiSelect => {
Box::new(MultiSelectTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Checkbox => {
Box::new(CheckboxTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::URL => {
Box::new(URLTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
FieldType::Checklist => {
Box::new(ChecklistTypeOptionPB::from_json_str(type_option_data)) as Box<dyn TypeOptionTransformHandler>
}
}
}

View File

@ -1,5 +1,4 @@
#![allow(clippy::module_inception)]
mod url_filter;
mod url_tests;
mod url_type_option;
mod url_type_option_entities;

View File

@ -1,18 +0,0 @@
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::field::{TypeOptionCellData, TypeOptionConfiguration, URLTypeOptionPB};
use flowy_error::FlowyResult;
impl CellFilterable for URLTypeOptionPB {
fn apply_filter(
&self,
type_cell_data: TypeCellData,
filter: &<Self as TypeOptionConfiguration>::CellFilterConfiguration,
) -> FlowyResult<bool> {
if !type_cell_data.is_url() {
return Ok(true);
}
let url_cell_data = self.decode_type_option_cell_str(type_cell_data.cell_str)?;
Ok(filter.is_visible(&url_cell_data))
}
}

View File

@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::cell::CellDataChangeset;
use crate::services::field::FieldBuilder;
use crate::services::field::{URLCellData, URLTypeOptionPB};
use crate::services::field::URLTypeOptionPB;
use grid_rev_model::FieldRevision;
/// The expected_str will equal to the input string, but the expected_url will be empty if there's no
@ -14,8 +14,8 @@ mod tests {
let type_option = URLTypeOptionPB::default();
let field_type = FieldType::URL;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_url(&type_option, "123", "123", "", &field_type, &field_rev);
assert_url(&type_option, "", "", "", &field_type, &field_rev);
assert_url(&type_option, "123", "123", "", &field_rev);
assert_url(&type_option, "", "", "", &field_rev);
}
/// The expected_str will equal to the input string, but the expected_url will not be empty
@ -30,7 +30,6 @@ mod tests {
"AppFlowy website - https://www.appflowy.io",
"AppFlowy website - https://www.appflowy.io",
"https://www.appflowy.io/",
&field_type,
&field_rev,
);
@ -39,7 +38,6 @@ mod tests {
"AppFlowy website appflowy.io",
"AppFlowy website appflowy.io",
"https://appflowy.io",
&field_type,
&field_rev,
);
}
@ -55,7 +53,6 @@ mod tests {
"AppFlowy website - https://www.appflowy.io welcome!",
"AppFlowy website - https://www.appflowy.io welcome!",
"https://www.appflowy.io/",
&field_type,
&field_rev,
);
@ -64,7 +61,6 @@ mod tests {
"AppFlowy website appflowy.io welcome!",
"AppFlowy website appflowy.io welcome!",
"https://appflowy.io",
&field_type,
&field_rev,
);
}
@ -80,7 +76,6 @@ mod tests {
"AppFlowy website - https://www.appflowy.io!",
"AppFlowy website - https://www.appflowy.io!",
"https://www.appflowy.io/",
&field_type,
&field_rev,
);
@ -89,7 +84,6 @@ mod tests {
"AppFlowy website appflowy.io!",
"AppFlowy website appflowy.io!",
"https://appflowy.io",
&field_type,
&field_rev,
);
}
@ -105,7 +99,6 @@ mod tests {
"test - https://tester.testgroup.appflowy.io",
"test - https://tester.testgroup.appflowy.io",
"https://tester.testgroup.appflowy.io/",
&field_type,
&field_rev,
);
@ -114,7 +107,6 @@ mod tests {
"test tester.testgroup.appflowy.io",
"test tester.testgroup.appflowy.io",
"https://tester.testgroup.appflowy.io",
&field_type,
&field_rev,
);
}
@ -130,7 +122,6 @@ mod tests {
"appflowy - https://appflowy.com",
"appflowy - https://appflowy.com",
"https://appflowy.com/",
&field_type,
&field_rev,
);
@ -139,7 +130,6 @@ mod tests {
"appflowy - https://appflowy.top",
"appflowy - https://appflowy.top",
"https://appflowy.top/",
&field_type,
&field_rev,
);
@ -148,7 +138,6 @@ mod tests {
"appflowy - https://appflowy.net",
"appflowy - https://appflowy.net",
"https://appflowy.net/",
&field_type,
&field_rev,
);
@ -157,7 +146,6 @@ mod tests {
"appflowy - https://appflowy.edu",
"appflowy - https://appflowy.edu",
"https://appflowy.edu/",
&field_type,
&field_rev,
);
}
@ -167,23 +155,10 @@ mod tests {
input_str: &str,
expected_str: &str,
expected_url: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
_field_rev: &FieldRevision,
) {
let encoded_data = type_option.apply_changeset(input_str.to_owned(), None).unwrap();
let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type);
let decode_cell_data = type_option.apply_changeset(input_str.to_owned(), None).unwrap();
assert_eq!(expected_str.to_owned(), decode_cell_data.content);
assert_eq!(expected_url.to_owned(), decode_cell_data.url);
}
fn decode_cell_data(
encoded_data: String,
type_option: &URLTypeOptionPB,
field_rev: &FieldRevision,
field_type: &FieldType,
) -> URLCellData {
type_option
.decode_cell_str(encoded_data, field_type, field_rev)
.unwrap()
}
}

View File

@ -2,8 +2,8 @@ use crate::entities::{FieldType, TextFilterPB};
use crate::impl_type_option;
use crate::services::cell::{CellDataChangeset, CellDataDecoder, FromCellString, TypeCellData};
use crate::services::field::{
BoxTypeOptionBuilder, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionConfiguration,
TypeOptionTransform, URLCellData, URLCellDataPB,
BoxTypeOptionBuilder, TypeOption, TypeOptionBuilder, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, URLCellData, URLCellDataPB,
};
use bytes::Bytes;
use fancy_regex::Regex;
@ -12,6 +12,7 @@ use flowy_error::FlowyResult;
use grid_rev_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Default)]
pub struct URLTypeOptionBuilder(URLTypeOptionPB);
@ -39,14 +40,11 @@ impl TypeOption for URLTypeOptionPB {
type CellData = URLCellData;
type CellChangeset = URLCellChangeset;
type CellProtobufType = URLCellDataPB;
type CellFilter = TextFilterPB;
}
impl TypeOptionTransform for URLTypeOptionPB {}
impl TypeOptionConfiguration for URLTypeOptionPB {
type CellFilterConfiguration = TextFilterPB;
}
impl TypeOptionCellData for URLTypeOptionPB {
fn convert_to_protobuf(&self, cell_data: <Self as TypeOption>::CellData) -> <Self as TypeOption>::CellProtobufType {
cell_data.into()
@ -83,19 +81,42 @@ impl CellDataChangeset for URLTypeOptionPB {
&self,
changeset: <Self as TypeOption>::CellChangeset,
_type_cell_data: Option<TypeCellData>,
) -> FlowyResult<String> {
) -> FlowyResult<<Self as TypeOption>::CellData> {
let mut url = "".to_string();
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
url = auto_append_scheme(m.as_str());
}
URLCellData {
Ok(URLCellData {
url,
content: changeset,
}
.to_json()
})
}
}
impl TypeOptionCellDataFilter for URLTypeOptionPB {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_url() {
return true;
}
filter.is_visible(&cell_data)
}
}
impl TypeOptionCellDataCompare for URLTypeOptionPB {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.content.cmp(&other_cell_data.content)
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(s) {

View File

@ -44,7 +44,7 @@ impl URLCellData {
}
}
pub(crate) fn to_json(&self) -> FlowyResult<String> {
pub fn to_json(&self) -> FlowyResult<String> {
serde_json::to_string(self).map_err(internal_error)
}
}

View File

@ -1,117 +0,0 @@
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, SelectOptionFilterPB, TextFilterPB,
};
use crate::services::filter::FilterType;
use std::collections::HashMap;
#[derive(Default, Debug)]
pub(crate) struct FilterMap {
pub(crate) text_filter: HashMap<FilterType, TextFilterPB>,
pub(crate) url_filter: HashMap<FilterType, TextFilterPB>,
pub(crate) number_filter: HashMap<FilterType, NumberFilterPB>,
pub(crate) date_filter: HashMap<FilterType, DateFilterPB>,
pub(crate) select_option_filter: HashMap<FilterType, SelectOptionFilterPB>,
pub(crate) checkbox_filter: HashMap<FilterType, CheckboxFilterPB>,
pub(crate) checklist_filter: HashMap<FilterType, ChecklistFilterPB>,
}
impl FilterMap {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn has_filter(&self, filter_type: &FilterType) -> bool {
match filter_type.field_type {
FieldType::RichText => self.text_filter.get(filter_type).is_some(),
FieldType::Number => self.number_filter.get(filter_type).is_some(),
FieldType::DateTime => self.date_filter.get(filter_type).is_some(),
FieldType::SingleSelect => self.select_option_filter.get(filter_type).is_some(),
FieldType::MultiSelect => self.select_option_filter.get(filter_type).is_some(),
FieldType::Checkbox => self.checkbox_filter.get(filter_type).is_some(),
FieldType::URL => self.url_filter.get(filter_type).is_some(),
FieldType::Checklist => self.checklist_filter.get(filter_type).is_some(),
}
}
pub(crate) fn is_empty(&self) -> bool {
if !self.text_filter.is_empty() {
return false;
}
if !self.url_filter.is_empty() {
return false;
}
if !self.number_filter.is_empty() {
return false;
}
if !self.number_filter.is_empty() {
return false;
}
if !self.date_filter.is_empty() {
return false;
}
if !self.select_option_filter.is_empty() {
return false;
}
if !self.checkbox_filter.is_empty() {
return false;
}
if !self.checklist_filter.is_empty() {
return false;
}
true
}
pub(crate) fn remove(&mut self, filter_id: &FilterType) {
let _ = match filter_id.field_type {
FieldType::RichText => {
let _ = self.text_filter.remove(filter_id);
}
FieldType::Number => {
let _ = self.number_filter.remove(filter_id);
}
FieldType::DateTime => {
let _ = self.date_filter.remove(filter_id);
}
FieldType::SingleSelect => {
let _ = self.select_option_filter.remove(filter_id);
}
FieldType::MultiSelect => {
let _ = self.select_option_filter.remove(filter_id);
}
FieldType::Checkbox => {
let _ = self.checkbox_filter.remove(filter_id);
}
FieldType::URL => {
let _ = self.url_filter.remove(filter_id);
}
FieldType::Checklist => {
let _ = self.checklist_filter.remove(filter_id);
}
};
}
}
/// Refresh the filter according to the field id.
#[derive(Default)]
pub(crate) struct FilterResult {
pub(crate) visible_by_filter_id: HashMap<FilterType, bool>,
}
impl FilterResult {
pub(crate) fn is_visible(&self) -> bool {
let mut is_visible = true;
for visible in self.visible_by_filter_id.values() {
if !is_visible {
break;
}
is_visible = *visible;
}
is_visible
}
}

View File

@ -1,8 +1,8 @@
use crate::entities::filter_entities::*;
use crate::entities::{FieldType, InsertedRowPB, RowPB};
use crate::services::cell::{CellFilterable, TypeCellData};
use crate::services::cell::{AnyTypeCache, AtomicCellDataCache, AtomicCellFilterCache, TypeCellData};
use crate::services::field::*;
use crate::services::filter::{FilterChangeset, FilterMap, FilterResult, FilterResultNotification, FilterType};
use crate::services::filter::{FilterChangeset, FilterResult, FilterResultNotification, FilterType};
use crate::services::row::GridBlockRowRevision;
use crate::services::view_editor::{GridViewChanged, GridViewChangedNotifier};
use flowy_error::FlowyResult;
@ -24,12 +24,19 @@ pub trait FilterDelegate: Send + Sync + 'static {
fn get_row_rev(&self, rows_id: &str) -> Fut<Option<(usize, Arc<RowRevision>)>>;
}
pub trait FromFilterString {
fn from_filter_rev(filter_rev: &FilterRevision) -> Self
where
Self: Sized;
}
pub struct FilterController {
view_id: String,
handler_id: String,
delegate: Box<dyn FilterDelegate>,
filter_map: FilterMap,
result_by_row_id: HashMap<RowId, FilterResult>,
cell_data_cache: AtomicCellDataCache,
cell_filter_cache: AtomicCellFilterCache,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
notifier: GridViewChangedNotifier,
}
@ -41,6 +48,7 @@ impl FilterController {
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
filter_revs: Vec<Arc<FilterRevision>>,
cell_data_cache: AtomicCellDataCache,
notifier: GridViewChangedNotifier,
) -> Self
where
@ -50,8 +58,9 @@ impl FilterController {
view_id: view_id.to_string(),
handler_id: handler_id.to_string(),
delegate: Box::new(delegate),
filter_map: FilterMap::new(),
result_by_row_id: HashMap::default(),
cell_data_cache,
cell_filter_cache: AnyTypeCache::<FilterType>::new(),
task_scheduler,
notifier,
};
@ -75,16 +84,17 @@ impl FilterController {
}
pub async fn filter_row_revs(&mut self, row_revs: &mut Vec<Arc<RowRevision>>) {
if self.filter_map.is_empty() {
if self.cell_filter_cache.read().is_empty() {
return;
}
let field_rev_by_field_id = self.get_filter_revs_map().await;
row_revs.iter().for_each(|row_rev| {
let _ = filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
&self.cell_data_cache,
&self.cell_filter_cache,
);
});
@ -121,9 +131,10 @@ impl FilterController {
let mut notification = FilterResultNotification::new(self.view_id.clone(), row_rev.block_id.clone());
if let Some((row_id, is_visible)) = filter_row(
&row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
&self.cell_data_cache,
&self.cell_filter_cache,
) {
if is_visible {
if let Some((index, row_rev)) = self.delegate.get_row_rev(&row_id).await {
@ -154,9 +165,10 @@ impl FilterController {
for (index, row_rev) in block.row_revs.iter().enumerate() {
if let Some((row_id, is_visible)) = filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
&self.cell_data_cache,
&self.cell_filter_cache,
) {
if is_visible {
let row_pb = RowPB::from(row_rev.as_ref());
@ -233,7 +245,7 @@ impl FilterController {
if let Some(filter) = self.filter_from_filter_type(filter_type).await {
notification = Some(FilterChangesetNotificationPB::from_delete(&self.view_id, vec![filter]));
}
self.filter_map.remove(filter_type);
self.cell_filter_cache.write().remove(filter_type);
}
let _ = self
@ -258,46 +270,39 @@ impl FilterController {
tracing::trace!("Create filter with type: {:?}", filter_type);
match &filter_type.field_type {
FieldType::RichText => {
let _ = self
.filter_map
.text_filter
.insert(filter_type, TextFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, TextFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::Number => {
let _ = self
.filter_map
.number_filter
.insert(filter_type, NumberFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, NumberFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::DateTime => {
let _ = self
.filter_map
.date_filter
.insert(filter_type, DateFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, DateFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::SingleSelect | FieldType::MultiSelect => {
let _ = self
.filter_map
.select_option_filter
.insert(filter_type, SelectOptionFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, SelectOptionFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::Checkbox => {
let _ = self
.filter_map
.checkbox_filter
.insert(filter_type, CheckboxFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, CheckboxFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::URL => {
let _ = self
.filter_map
.url_filter
.insert(filter_type, TextFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, TextFilterPB::from_filter_rev(filter_rev.as_ref()));
}
FieldType::Checklist => {
let _ = self
.filter_map
.checklist_filter
.insert(filter_type, ChecklistFilterPB::from(filter_rev.as_ref()));
self.cell_filter_cache
.write()
.insert(&filter_type, ChecklistFilterPB::from_filter_rev(filter_rev.as_ref()));
}
}
}
@ -309,9 +314,10 @@ impl FilterController {
#[tracing::instrument(level = "trace", skip_all)]
fn filter_row(
row_rev: &Arc<RowRevision>,
filter_map: &FilterMap,
result_by_row_id: &mut HashMap<RowId, FilterResult>,
field_rev_by_field_id: &HashMap<FieldId, Arc<FieldRevision>>,
cell_data_cache: &AtomicCellDataCache,
cell_filter_cache: &AtomicCellFilterCache,
) -> Option<(String, bool)> {
// Create a filter result cache if it's not exist
let filter_result = result_by_row_id
@ -322,7 +328,7 @@ fn filter_row(
// Iterate each cell of the row to check its visibility
for (field_id, field_rev) in field_rev_by_field_id {
let filter_type = FilterType::from(field_rev);
if !filter_map.has_filter(&filter_type) {
if !cell_filter_cache.read().contains(&filter_type) {
filter_result.visible_by_filter_id.remove(&filter_type);
continue;
}
@ -330,7 +336,7 @@ fn filter_row(
let cell_rev = row_rev.cells.get(field_id);
// if the visibility of the cell_rew is changed, which means the visibility of the
// row is changed too.
if let Some(is_visible) = filter_cell(&filter_type, field_rev, filter_map, cell_rev) {
if let Some(is_visible) = filter_cell(&filter_type, field_rev, cell_rev, cell_data_cache, cell_filter_cache) {
filter_result.visible_by_filter_id.insert(filter_type, is_visible);
}
}
@ -348,93 +354,32 @@ fn filter_row(
#[tracing::instrument(level = "trace", skip_all, fields(cell_content))]
fn filter_cell(
filter_id: &FilterType,
filter_type: &FilterType,
field_rev: &Arc<FieldRevision>,
filter_map: &FilterMap,
cell_rev: Option<&CellRevision>,
cell_data_cache: &AtomicCellDataCache,
cell_filter_cache: &AtomicCellFilterCache,
) -> Option<bool> {
let type_cell_data = match cell_rev {
None => TypeCellData::from_field_type(&filter_id.field_type),
None => TypeCellData::from_field_type(&filter_type.field_type),
Some(cell_rev) => match TypeCellData::try_from(cell_rev) {
Ok(cell_data) => cell_data,
Err(err) => {
tracing::error!("Deserialize TypeCellData failed: {}", err);
TypeCellData::from_field_type(&filter_id.field_type)
TypeCellData::from_field_type(&filter_type.field_type)
}
},
};
let cloned_type_cell_data = type_cell_data.cell_str.clone();
let is_visible = match &filter_id.field_type {
FieldType::RichText => filter_map.text_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<RichTextTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::Number => filter_map.number_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<NumberTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::DateTime => filter_map.date_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<DateTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::SingleSelect => filter_map.select_option_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<SingleSelectTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::MultiSelect => filter_map.select_option_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<MultiSelectTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::Checkbox => filter_map.checkbox_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<CheckboxTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::URL => filter_map.url_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<URLTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
FieldType::Checklist => filter_map.checklist_filter.get(filter_id).and_then(|filter| {
Some(
field_rev
.get_type_option::<ChecklistTypeOptionPB>(field_rev.ty)?
.apply_filter(type_cell_data, filter)
.ok(),
)
}),
}?;
tracing::Span::current().record(
"cell_content",
&format!("{} => {:?}", cloned_type_cell_data, is_visible.unwrap()).as_str(),
);
is_visible
let handler = TypeOptionCellExt::new(
field_rev.as_ref(),
Some(cell_data_cache.clone()),
Some(cell_filter_cache.clone()),
)
.get_type_option_cell_data_handler(&filter_type.field_type)?;
let is_visible = handler.handle_cell_filter(filter_type, field_rev.as_ref(), type_cell_data);
Some(is_visible)
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@ -1,9 +1,7 @@
mod cache;
mod controller;
mod entities;
mod task;
pub(crate) use cache::*;
pub use controller::*;
pub use entities::*;
pub(crate) use task::*;

View File

@ -1,6 +1,7 @@
use crate::services::filter::FilterController;
use crate::services::filter::{FilterController, FilterType};
use flowy_task::{TaskContent, TaskHandler};
use lib_infra::future::BoxResultFuture;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -38,3 +39,21 @@ impl TaskHandler for FilterTaskHandler {
})
}
}
/// Refresh the filter according to the field id.
#[derive(Default)]
pub(crate) struct FilterResult {
pub(crate) visible_by_filter_id: HashMap<FilterType, bool>,
}
impl FilterResult {
pub(crate) fn is_visible(&self) -> bool {
let mut is_visible = true;
for visible in self.visible_by_filter_id.values() {
if !is_visible {
break;
}
is_visible = *visible;
}
is_visible
}
}

View File

@ -3,7 +3,10 @@ use crate::entities::CellPathParams;
use crate::entities::*;
use crate::manager::GridUser;
use crate::services::block_manager::GridBlockManager;
use crate::services::cell::{apply_cell_data_changeset, decode_type_cell_data, CellProtobufBlob};
use crate::services::cell::{
apply_cell_data_changeset, decode_type_cell_data, stringify_cell_data, AnyTypeCache, AtomicCellDataCache,
CellProtobufBlob, TypeCellData,
};
use crate::services::field::{
default_type_option_builder_from_type, transform_type_option, type_option_builder_from_bytes, FieldBuilder,
};
@ -39,6 +42,7 @@ pub struct GridRevisionEditor {
view_manager: Arc<GridViewManager>,
rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
block_manager: Arc<GridBlockManager>,
cell_data_cache: AtomicCellDataCache,
}
impl Drop for GridRevisionEditor {
@ -60,6 +64,7 @@ impl GridRevisionEditor {
let grid_pad = rev_manager.initialize::<GridRevisionSerde>(Some(cloud)).await?;
let rev_manager = Arc::new(rev_manager);
let grid_pad = Arc::new(RwLock::new(grid_pad));
let cell_data_cache = AnyTypeCache::<u64>::new();
// Block manager
let (block_event_tx, block_event_rx) = broadcast::channel(100);
@ -72,8 +77,16 @@ impl GridRevisionEditor {
});
// View manager
let view_manager =
Arc::new(GridViewManager::new(grid_id.to_owned(), user.clone(), delegate, block_event_rx).await?);
let view_manager = Arc::new(
GridViewManager::new(
grid_id.to_owned(),
user.clone(),
delegate,
cell_data_cache.clone(),
block_event_rx,
)
.await?,
);
let editor = Arc::new(Self {
grid_id: grid_id.to_owned(),
@ -82,6 +95,7 @@ impl GridRevisionEditor {
rev_manager,
block_manager,
view_manager,
cell_data_cache,
});
Ok(editor)
@ -430,6 +444,23 @@ impl GridRevisionEditor {
Some(CellPB::new(&params.field_id, field_type, cell_bytes.to_vec()))
}
pub async fn get_cell_display_str(&self, params: &CellPathParams) -> String {
let display_str = || async {
let field_rev = self.get_field_rev(&params.field_id).await?;
let field_type: FieldType = field_rev.ty.into();
let cell_rev = self.get_cell_rev(&params.row_id, &params.field_id).await.ok()??;
let type_cell_data: TypeCellData = cell_rev.try_into().ok()?;
Some(stringify_cell_data(
type_cell_data.cell_str,
&field_type,
&field_type,
&field_rev,
))
};
display_str().await.unwrap_or("".to_string())
}
pub async fn get_cell_bytes(&self, params: &CellPathParams) -> Option<CellProtobufBlob> {
let (_, cell_data) = self.decode_cell_data_from(params).await?;
Some(cell_data)
@ -439,7 +470,11 @@ impl GridRevisionEditor {
let field_rev = self.get_field_rev(&params.field_id).await?;
let (_, row_rev) = self.block_manager.get_row_rev(&params.row_id).await.ok()??;
let cell_rev = row_rev.cells.get(&params.field_id)?.clone();
Some(decode_type_cell_data(cell_rev.type_cell_data, &field_rev))
Some(decode_type_cell_data(
cell_rev.type_cell_data,
&field_rev,
Some(self.cell_data_cache.clone()),
))
}
pub async fn get_cell_rev(&self, row_id: &str, field_id: &str) -> FlowyResult<Option<CellRevision>> {
@ -470,7 +505,7 @@ impl GridRevisionEditor {
tracing::trace!("field changeset: id:{} / value:{:?}", &field_id, content);
let cell_rev = self.get_cell_rev(&row_id, &field_id).await?;
// Update the changeset.data property with the return value.
content = apply_cell_data_changeset(content, cell_rev, field_rev)?;
content = apply_cell_data_changeset(content, cell_rev, field_rev, Some(self.cell_data_cache.clone()))?;
let cell_changeset = CellChangesetPB {
grid_id,
row_id: row_id.clone(),
@ -588,9 +623,9 @@ impl GridRevisionEditor {
Ok(())
}
pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult<()> {
let _ = self.view_manager.create_or_update_sort(params).await?;
Ok(())
pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult<SortRevision> {
let sort_rev = self.view_manager.create_or_update_sort(params).await?;
Ok(sort_rev)
}
pub async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> {

View File

@ -184,7 +184,7 @@ where
if let Some(cell_rev) = cell_rev {
let mut grouped_rows: Vec<GroupedRow> = vec![];
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, field_rev).1;
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, field_rev, None).1;
let cell_data = cell_bytes.parser::<P>()?;
for group in self.group_ctx.groups() {
if self.can_group(&group.filter_content, &cell_data) {
@ -224,7 +224,7 @@ where
field_rev: &FieldRevision,
) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev).1;
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev, None).1;
let cell_data = cell_bytes.parser::<P>()?;
let mut changesets = self.add_or_remove_row_in_groups_if_match(row_rev, &cell_data);
@ -247,7 +247,7 @@ where
) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
// if the cell_rev is none, then the row must in the default group.
if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev).1;
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev, None).1;
let cell_data = cell_bytes.parser::<P>()?;
if !cell_data.is_empty() {
tracing::error!("did_delete_delete_row {:?}", cell_rev.type_cell_data);
@ -280,7 +280,7 @@ where
};
if let Some(cell_rev) = cell_rev {
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, context.field_rev).1;
let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, context.field_rev, None).1;
let cell_data = cell_bytes.parser::<P>()?;
Ok(self.move_row(&cell_data, context))
} else {

View File

@ -3,50 +3,50 @@
use crate::entities::FieldType;
#[allow(unused_attributes)]
use crate::entities::SortChangesetNotificationPB;
use crate::services::sort::{SortChangeset, SortType};
use flowy_task::TaskDispatcher;
use grid_rev_model::{CellRevision, FieldRevision, RowRevision, SortCondition, SortRevision};
use lib_infra::future::Fut;
use crate::services::cell::{AtomicCellDataCache, TypeCellData};
use crate::services::field::{default_order, TypeOptionCellExt};
use rayon::prelude::ParallelSliceMut;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub trait SortDelegate: Send + Sync {
fn get_sort_rev(&self, sort_type: SortType) -> Fut<Vec<Arc<SortRevision>>>;
fn get_sort_rev(&self, sort_type: SortType) -> Fut<Option<Arc<SortRevision>>>;
fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>>;
fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>>;
}
pub struct SortController {
#[allow(dead_code)]
view_id: String,
#[allow(dead_code)]
handler_id: String,
#[allow(dead_code)]
delegate: Box<dyn SortDelegate>,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
#[allow(dead_code)]
sorts: Vec<SortRevision>,
#[allow(dead_code)]
row_orders: HashMap<String, usize>,
sorts: Vec<Arc<SortRevision>>,
cell_data_cache: AtomicCellDataCache,
}
impl SortController {
pub fn new<T>(view_id: &str, handler_id: &str, delegate: T, task_scheduler: Arc<RwLock<TaskDispatcher>>) -> Self
pub fn new<T>(
_view_id: &str,
handler_id: &str,
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
cell_data_cache: AtomicCellDataCache,
) -> Self
where
T: SortDelegate + 'static,
{
Self {
view_id: view_id.to_string(),
handler_id: handler_id.to_string(),
delegate: Box::new(delegate),
task_scheduler,
sorts: vec![],
row_orders: HashMap::new(),
cell_data_cache,
}
}
@ -58,35 +58,53 @@ impl SortController {
.await;
}
pub fn sort_rows(&self, _rows: &mut Vec<Arc<RowRevision>>) {
// rows.par_sort_by(|left, right| cmp_row(left, right, &self.sorts));
pub async fn sort_rows(&self, rows: &mut Vec<Arc<RowRevision>>) {
let field_revs = self.delegate.get_field_revs(None).await;
rows.par_sort_by(|left, right| cmp_row(left, right, &self.sorts, &field_revs, &self.cell_data_cache));
}
pub async fn did_receive_changes(&mut self, _changeset: SortChangeset) -> Option<SortChangesetNotificationPB> {
#[tracing::instrument(level = "trace", skip(self))]
pub async fn did_receive_changes(&mut self, changeset: SortChangeset) -> Option<SortChangesetNotificationPB> {
if let Some(insert_sort) = changeset.insert_sort {
if let Some(sort) = self.delegate.get_sort_rev(insert_sort).await {
self.sorts.push(sort);
}
}
if let Some(delete_sort_type) = changeset.delete_sort {
if let Some(index) = self.sorts.iter().position(|sort| sort.id == delete_sort_type.sort_id) {
self.sorts.remove(index);
}
}
if let Some(_update_sort) = changeset.update_sort {
//
}
None
}
}
#[allow(dead_code)]
fn cmp_row(
left: &Arc<RowRevision>,
right: &Arc<RowRevision>,
sorts: &[SortRevision],
sorts: &[Arc<SortRevision>],
field_revs: &[Arc<FieldRevision>],
cell_data_cache: &AtomicCellDataCache,
) -> Ordering {
let mut order = Ordering::Equal;
let mut order = default_order();
for sort in sorts.iter() {
let cmp_order = match (left.cells.get(&sort.field_id), right.cells.get(&sort.field_id)) {
(Some(left_cell), Some(right_cell)) => {
let field_type: FieldType = sort.field_type.into();
match field_revs.iter().find(|field_rev| field_rev.id == sort.field_id) {
None => Ordering::Equal,
Some(field_rev) => cmp_cell(left_cell, right_cell, field_rev, field_type),
None => default_order(),
Some(field_rev) => cmp_cell(left_cell, right_cell, field_rev, field_type, cell_data_cache),
}
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
_ => Ordering::Equal,
_ => default_order(),
};
if cmp_order.is_ne() {
@ -101,29 +119,26 @@ fn cmp_row(
order
}
#[allow(dead_code)]
fn cmp_cell(
_left: &CellRevision,
_right: &CellRevision,
_field_rev: &Arc<FieldRevision>,
left_cell: &CellRevision,
right_cell: &CellRevision,
field_rev: &Arc<FieldRevision>,
field_type: FieldType,
cell_data_cache: &AtomicCellDataCache,
) -> Ordering {
let cal_order = || {
let order = match &field_type {
// FieldType::RichText => {
// let left_cell = TypeCellData::try_from(left).ok()?.into();
// let right_cell = TypeCellData::try_from(right).ok()?.into();
// field_rev
// .get_type_option::<RichTextTypeOptionPB>(field_rev.ty)?
// .apply_cmp(&left_cell, &right_cell)
// }
// FieldType::Number => field_rev
// .get_type_option::<NumberTypeOptionPB>(field_rev.ty)?
// .apply_cmp(&left_cell, &right_cell),
_ => Ordering::Equal,
};
Option::<Ordering>::Some(order)
};
match TypeOptionCellExt::new_with_cell_data_cache(field_rev.as_ref(), Some(cell_data_cache.clone()))
.get_type_option_cell_data_handler(&field_type)
{
None => Ordering::Less,
Some(handler) => {
let cal_order = || {
let left_cell_str = TypeCellData::try_from(left_cell).ok()?.into_inner();
let right_cell_str = TypeCellData::try_from(right_cell).ok()?.into_inner();
let order = handler.handle_cell_compare(&left_cell_str, &right_cell_str, field_rev.as_ref());
Option::<Ordering>::Some(order)
};
cal_order().unwrap_or(Ordering::Equal)
cal_order().unwrap_or_else(default_order)
}
}
}

View File

@ -1,4 +1,4 @@
use crate::entities::{AlterSortParams, FieldType};
use crate::entities::{AlterSortParams, DeleteSortParams, FieldType};
use grid_rev_model::{FieldRevision, FieldTypeRevision};
use std::sync::Arc;
@ -37,7 +37,7 @@ impl std::convert::From<&Arc<FieldRevision>> for SortType {
pub struct SortChangeset {
pub(crate) insert_sort: Option<SortType>,
pub(crate) update_sort: Option<SortType>,
pub(crate) delete_sort: Option<SortType>,
pub(crate) delete_sort: Option<DeletedSortType>,
}
impl SortChangeset {
@ -57,11 +57,26 @@ impl SortChangeset {
}
}
pub fn from_delete(sort: SortType) -> Self {
pub fn from_delete(deleted_sort: DeletedSortType) -> Self {
Self {
insert_sort: None,
update_sort: None,
delete_sort: Some(sort),
delete_sort: Some(deleted_sort),
}
}
}
#[derive(Debug)]
pub struct DeletedSortType {
pub sort_type: SortType,
pub sort_id: String,
}
impl std::convert::From<DeleteSortParams> for DeletedSortType {
fn from(params: DeleteSortParams) -> Self {
Self {
sort_type: params.sort_type,
sort_id: params.sort_id,
}
}
}

View File

@ -1,13 +1,14 @@
use crate::dart_notification::{send_dart_notification, GridDartNotification};
use crate::entities::*;
use crate::services::block_manager::GridBlockEvent;
use crate::services::cell::AtomicCellDataCache;
use crate::services::filter::{FilterChangeset, FilterController, FilterTaskHandler, FilterType, UpdatedFilterType};
use crate::services::group::{
default_group_configuration, find_group_field, make_group_controller, Group, GroupConfigurationReader,
GroupController, MoveGroupRowContext,
};
use crate::services::row::GridBlockRowRevision;
use crate::services::sort::{SortChangeset, SortController, SortTaskHandler, SortType};
use crate::services::sort::{DeletedSortType, SortChangeset, SortController, SortTaskHandler, SortType};
use crate::services::view_editor::changed_notifier::GridViewChangedNotifier;
use crate::services::view_editor::trait_impl::*;
use crate::services::view_editor::GridViewChangedReceiverRunner;
@ -78,6 +79,7 @@ impl GridViewRevisionEditor {
token: &str,
view_id: String,
delegate: Arc<dyn GridViewEditorDelegate>,
cell_data_cache: AtomicCellDataCache,
mut rev_manager: RevisionManager<Arc<ConnectionPool>>,
) -> FlowyResult<Self> {
let (notifier, _) = broadcast::channel(100);
@ -110,12 +112,24 @@ impl GridViewRevisionEditor {
)
.await?;
let sort_controller = make_sort_controller(&view_id, delegate.clone(), view_rev_pad.clone()).await;
let sort_controller = make_sort_controller(
&view_id,
delegate.clone(),
view_rev_pad.clone(),
cell_data_cache.clone(),
)
.await;
let user_id = user_id.to_owned();
let group_controller = Arc::new(RwLock::new(group_controller));
let filter_controller =
make_filter_controller(&view_id, delegate.clone(), notifier.clone(), view_rev_pad.clone()).await;
let filter_controller = make_filter_controller(
&view_id,
delegate.clone(),
notifier.clone(),
cell_data_cache,
view_rev_pad.clone(),
)
.await;
Ok(Self {
pad: view_rev_pad,
user_id,
@ -167,7 +181,7 @@ impl GridViewRevisionEditor {
}
pub async fn sort_rows(&self, rows: &mut Vec<Arc<RowRevision>>) {
self.sort_controller.read().await.sort_rows(rows)
self.sort_controller.read().await.sort_rows(rows).await
}
pub async fn filter_rows(&self, _block_id: &str, mut rows: Vec<Arc<RowRevision>>) -> Vec<Arc<RowRevision>> {
@ -374,7 +388,7 @@ impl GridViewRevisionEditor {
.await
}
pub async fn insert_view_sort(&self, params: AlterSortParams) -> FlowyResult<()> {
pub async fn insert_view_sort(&self, params: AlterSortParams) -> FlowyResult<SortRevision> {
let sort_type = SortType::from(&params);
let is_exist = params.sort_id.is_some();
let sort_id = match params.sort_id {
@ -392,7 +406,7 @@ impl GridViewRevisionEditor {
let mut sort_controller = self.sort_controller.write().await;
let changeset = if is_exist {
self.modify(|pad| {
let changeset = pad.update_sort(&params.field_id, sort_rev)?;
let changeset = pad.update_sort(&params.field_id, sort_rev.clone())?;
Ok(changeset)
})
.await?;
@ -401,7 +415,7 @@ impl GridViewRevisionEditor {
.await
} else {
self.modify(|pad| {
let changeset = pad.insert_sort(&params.field_id, sort_rev)?;
let changeset = pad.insert_sort(&params.field_id, sort_rev.clone())?;
Ok(changeset)
})
.await?;
@ -413,18 +427,18 @@ impl GridViewRevisionEditor {
if let Some(changeset) = changeset {
self.notify_did_update_sort(changeset).await;
}
Ok(())
Ok(sort_rev)
}
pub async fn delete_view_sort(&self, params: DeleteSortParams) -> FlowyResult<()> {
let sort_type = params.sort_type;
let changeset = self
.sort_controller
.write()
.await
.did_receive_changes(SortChangeset::from_delete(sort_type.clone()))
.did_receive_changes(SortChangeset::from_delete(DeletedSortType::from(params.clone())))
.await;
let sort_type = params.sort_type;
let _ = self
.modify(|pad| {
let changeset = pad.delete_sort(&params.sort_id, &sort_type.field_id, sort_type.field_type)?;
@ -710,6 +724,7 @@ async fn make_filter_controller(
view_id: &str,
delegate: Arc<dyn GridViewEditorDelegate>,
notifier: GridViewChangedNotifier,
cell_data_cache: AtomicCellDataCache,
pad: Arc<RwLock<GridViewRevisionPad>>,
) -> Arc<RwLock<FilterController>> {
let field_revs = delegate.get_field_revs(None).await;
@ -726,6 +741,7 @@ async fn make_filter_controller(
filter_delegate,
task_scheduler.clone(),
filter_revs,
cell_data_cache,
notifier,
)
.await;
@ -741,6 +757,7 @@ async fn make_sort_controller(
view_id: &str,
delegate: Arc<dyn GridViewEditorDelegate>,
pad: Arc<RwLock<GridViewRevisionPad>>,
cell_data_cache: AtomicCellDataCache,
) -> Arc<RwLock<SortController>> {
let handler_id = gen_handler_id();
let sort_delegate = GridViewSortDelegateImpl {
@ -753,6 +770,7 @@ async fn make_sort_controller(
&handler_id,
sort_delegate,
task_scheduler.clone(),
cell_data_cache,
)));
task_scheduler
.write()

View File

@ -4,6 +4,7 @@ use crate::entities::{
};
use crate::manager::GridUser;
use crate::services::block_manager::GridBlockEvent;
use crate::services::cell::AtomicCellDataCache;
use crate::services::filter::FilterType;
use crate::services::persistence::rev_sqlite::{
SQLiteGridRevisionSnapshotPersistence, SQLiteGridViewRevisionPersistence,
@ -14,7 +15,7 @@ use crate::services::view_editor::{GridViewEditorDelegate, GridViewRevisionEdito
use flowy_database::ConnectionPool;
use flowy_error::FlowyResult;
use flowy_revision::{RevisionManager, RevisionPersistence, RevisionPersistenceConfiguration};
use grid_rev_model::{FieldRevision, FilterRevision, RowChangeset, RowRevision};
use grid_rev_model::{FieldRevision, FilterRevision, RowChangeset, RowRevision, SortRevision};
use lib_infra::future::Fut;
use lib_infra::ref_map::RefCountHashMap;
use std::borrow::Cow;
@ -26,6 +27,7 @@ pub struct GridViewManager {
user: Arc<dyn GridUser>,
delegate: Arc<dyn GridViewEditorDelegate>,
view_editors: Arc<RwLock<RefCountHashMap<Arc<GridViewRevisionEditor>>>>,
cell_data_cache: AtomicCellDataCache,
}
impl GridViewManager {
@ -33,6 +35,7 @@ impl GridViewManager {
grid_id: String,
user: Arc<dyn GridUser>,
delegate: Arc<dyn GridViewEditorDelegate>,
cell_data_cache: AtomicCellDataCache,
block_event_rx: broadcast::Receiver<GridBlockEvent>,
) -> FlowyResult<Self> {
let view_editors = Arc::new(RwLock::new(RefCountHashMap::default()));
@ -41,6 +44,7 @@ impl GridViewManager {
grid_id,
user,
delegate,
cell_data_cache,
view_editors,
})
}
@ -145,7 +149,7 @@ impl GridViewManager {
view_editor.delete_view_filter(params).await
}
pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult<()> {
pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult<SortRevision> {
let view_editor = self.get_view_editor(&params.view_id).await?;
view_editor.insert_view_sort(params).await
}
@ -250,7 +254,15 @@ impl GridViewManager {
let token = self.user.token()?;
let view_id = view_id.to_owned();
GridViewRevisionEditor::new(&user_id, &token, view_id, self.delegate.clone(), rev_manager).await
GridViewRevisionEditor::new(
&user_id,
&token,
view_id,
self.delegate.clone(),
self.cell_data_cache.clone(),
rev_manager,
)
.await
}
}

View File

@ -172,11 +172,18 @@ pub(crate) struct GridViewSortDelegateImpl {
}
impl SortDelegate for GridViewSortDelegateImpl {
fn get_sort_rev(&self, sort_type: SortType) -> Fut<Vec<Arc<SortRevision>>> {
fn get_sort_rev(&self, sort_type: SortType) -> Fut<Option<Arc<SortRevision>>> {
let pad = self.view_revision_pad.clone();
to_fut(async move {
let field_type_rev: FieldTypeRevision = sort_type.field_type.into();
pad.read().await.get_sorts(&sort_type.field_id, &field_type_rev)
let mut sorts = pad.read().await.get_sorts(&sort_type.field_id, &field_type_rev);
if sorts.is_empty() {
None
} else {
// Currently, one sort_type should have one sort.
debug_assert_eq!(sorts.len(), 1);
sorts.pop()
}
})
}

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use flowy_grid::services::field::{
ChecklistTypeOptionPB, DateCellChangeset, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB,
URLCellData,
};
use flowy_grid::services::row::RowRevisionBuilder;
use grid_rev_model::{FieldRevision, RowRevision};
@ -59,7 +60,8 @@ impl<'a> GridRowTestBuilder<'a> {
pub fn insert_url_cell(&mut self, data: &str) -> String {
let url_field = self.field_rev_with_type(&FieldType::URL);
self.inner_builder.insert_text_cell(&url_field.id, data.to_string());
let url_data = URLCellData::new(data).to_json().unwrap();
self.inner_builder.insert_text_cell(&url_field.id, url_data);
url_field.id.clone()
}

View File

@ -53,7 +53,7 @@ async fn grid_filter_multi_select_is_test2() {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(1).id],
},
AssertNumberOfVisibleRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}

View File

@ -269,7 +269,7 @@ fn make_test_grid() -> BuildGridContext {
FieldType::Number => row_builder.insert_number_cell("2"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::MultiSelect => row_builder
.insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
.insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]),
FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
_ => "".to_owned(),
};

View File

@ -1,2 +1,2 @@
// mod script;
// mod text_sort_test;
mod script;
mod sort_test;

View File

@ -1,20 +1,32 @@
use crate::grid::grid_editor::GridEditorTest;
use flowy_grid::entities::{AlterSortParams, DeleteSortParams};
use flowy_grid::entities::{AlterSortParams, CellPathParams, DeleteSortParams};
use grid_rev_model::SortRevision;
pub enum SortScript {
InsertSort { params: AlterSortParams },
DeleteSort { params: DeleteSortParams },
AssertTextOrder { orders: Vec<String> },
InsertSort {
params: AlterSortParams,
},
DeleteSort {
params: DeleteSortParams,
},
AssertTextOrder {
field_id: String,
orders: Vec<&'static str>,
},
}
pub struct GridSortTest {
inner: GridEditorTest,
pub current_sort_rev: Option<SortRevision>,
}
impl GridSortTest {
pub async fn new() -> Self {
let editor_test = GridEditorTest::new_table().await;
Self { inner: editor_test }
Self {
inner: editor_test,
current_sort_rev: None,
}
}
pub async fn run_scripts(&mut self, scripts: Vec<SortScript>) {
for script in scripts {
@ -25,14 +37,26 @@ impl GridSortTest {
pub async fn run_script(&mut self, script: SortScript) {
match script {
SortScript::InsertSort { params } => {
let _ = self.editor.create_or_update_sort(params).await.unwrap();
let sort_rev = self.editor.create_or_update_sort(params).await.unwrap();
self.current_sort_rev = Some(sort_rev);
}
SortScript::DeleteSort { params } => {
//
self.editor.delete_sort(params).await.unwrap();
}
SortScript::AssertTextOrder { orders: _ } => {
//
SortScript::AssertTextOrder { field_id, orders } => {
let mut cells = vec![];
let rows = self.editor.get_grid(&self.grid_id).await.unwrap().rows;
for row in rows {
let params = CellPathParams {
view_id: self.grid_id.clone(),
field_id: field_id.clone(),
row_id: row.id,
};
let cell = self.editor.get_cell_display_str(&params).await;
cells.push(cell);
}
assert_eq!(cells, orders)
}
}
}

View File

@ -0,0 +1,279 @@
use crate::grid::sort_test::script::{GridSortTest, SortScript::*};
use flowy_grid::entities::{AlterSortParams, DeleteSortParams, FieldType};
use flowy_grid::services::sort::SortType;
use grid_rev_model::SortCondition;
#[tokio::test]
async fn sort_text_by_ascending_test() {
let mut test = GridSortTest::new().await;
let text_field = test.get_first_field_rev(FieldType::RichText);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: text_field.id.clone(),
sort_id: None,
field_type: FieldType::RichText.into(),
condition: SortCondition::Ascending.into(),
},
},
AssertTextOrder {
field_id: text_field.id.clone(),
orders: vec!["", "A", "AE", "C", "DA"],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_text_by_ascending_and_delete_sort_test() {
let mut test = GridSortTest::new().await;
let text_field = test.get_first_field_rev(FieldType::RichText).clone();
let view_id = test.grid_id.clone();
let scripts = vec![InsertSort {
params: AlterSortParams {
view_id: view_id.clone(),
field_id: text_field.id.clone(),
sort_id: None,
field_type: FieldType::RichText.into(),
condition: SortCondition::Ascending.into(),
},
}];
test.run_scripts(scripts).await;
let sort_rev = test.current_sort_rev.as_ref().unwrap();
let scripts = vec![
DeleteSort {
params: DeleteSortParams {
view_id,
sort_type: SortType::from(&text_field),
sort_id: sort_rev.id.clone(),
},
},
AssertTextOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE"],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_text_by_descending_test() {
let mut test = GridSortTest::new().await;
let text_field = test.get_first_field_rev(FieldType::RichText);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: text_field.id.clone(),
sort_id: None,
field_type: FieldType::RichText.into(),
condition: SortCondition::Descending.into(),
},
},
AssertTextOrder {
field_id: text_field.id.clone(),
orders: vec!["DA", "C", "AE", "A", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_checkbox_by_ascending_test() {
let mut test = GridSortTest::new().await;
let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: checkbox_field.id.clone(),
sort_id: None,
field_type: FieldType::Checkbox.into(),
condition: SortCondition::Ascending.into(),
},
},
// AssertTextOrder {
// field_id: checkbox_field.id.clone(),
// orders: vec!["No", "No", "No", "Yes", "Yes"],
// },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_checkbox_by_descending_test() {
let mut test = GridSortTest::new().await;
let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: checkbox_field.id.clone(),
sort_id: None,
field_type: FieldType::Checkbox.into(),
condition: SortCondition::Descending.into(),
},
},
AssertTextOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No"],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_date_by_ascending_test() {
let mut test = GridSortTest::new().await;
let date_field = test.get_first_field_rev(FieldType::DateTime);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: date_field.id.clone(),
orders: vec!["2022/03/14", "2022/03/14", "2022/03/14", "2022/11/17", "2022/11/13"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: date_field.id.clone(),
sort_id: None,
field_type: FieldType::DateTime.into(),
condition: SortCondition::Ascending.into(),
},
},
AssertTextOrder {
field_id: date_field.id.clone(),
orders: vec!["2022/03/14", "2022/03/14", "2022/03/14", "2022/11/13", "2022/11/17"],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_date_by_descending_test() {
let mut test = GridSortTest::new().await;
let date_field = test.get_first_field_rev(FieldType::DateTime);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: date_field.id.clone(),
orders: vec!["2022/03/14", "2022/03/14", "2022/03/14", "2022/11/17", "2022/11/13"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: date_field.id.clone(),
sort_id: None,
field_type: FieldType::DateTime.into(),
condition: SortCondition::Descending.into(),
},
},
AssertTextOrder {
field_id: date_field.id.clone(),
orders: vec!["2022/11/17", "2022/11/13", "2022/03/14", "2022/03/14", "2022/03/14"],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_number_by_descending_test() {
let mut test = GridSortTest::new().await;
let number_field = test.get_first_field_rev(FieldType::Number);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: number_field.id.clone(),
orders: vec!["$1", "$2", "$3", "$4", ""],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: number_field.id.clone(),
sort_id: None,
field_type: FieldType::Number.into(),
condition: SortCondition::Descending.into(),
},
},
AssertTextOrder {
field_id: number_field.id.clone(),
orders: vec!["$4", "$3", "$2", "$1", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_single_select_by_descending_test() {
let mut test = GridSortTest::new().await;
let single_select = test.get_first_field_rev(FieldType::SingleSelect);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: single_select.id.clone(),
orders: vec!["", "", "Completed", "Completed", "Planned"],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: single_select.id.clone(),
sort_id: None,
field_type: FieldType::SingleSelect.into(),
condition: SortCondition::Descending.into(),
},
},
AssertTextOrder {
field_id: single_select.id.clone(),
orders: vec!["Planned", "Completed", "Completed", "", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_multi_select_by_ascending_test() {
let mut test = GridSortTest::new().await;
let multi_select = test.get_first_field_rev(FieldType::MultiSelect);
let view_id = test.grid_id.clone();
let scripts = vec![
AssertTextOrder {
field_id: multi_select.id.clone(),
orders: vec!["Google,Facebook", "Google,Twitter", "Facebook", "", ""],
},
InsertSort {
params: AlterSortParams {
view_id,
field_id: multi_select.id.clone(),
sort_id: None,
field_type: FieldType::MultiSelect.into(),
condition: SortCondition::Ascending.into(),
},
},
AssertTextOrder {
field_id: multi_select.id.clone(),
orders: vec!["", "", "Facebook", "Google,Facebook", "Google,Twitter"],
},
];
test.run_scripts(scripts).await;
}

View File

@ -32,3 +32,9 @@ impl std::default::Default for SortCondition {
Self::Ascending
}
}
impl std::convert::From<SortCondition> for u8 {
fn from(condition: SortCondition) -> Self {
condition as u8
}
}