feat: selectOption sort + fix logic in edge cases (#3109)

This commit is contained in:
Richard Shiue 2023-08-09 12:43:03 +08:00 committed by GitHub
parent a5eaf15548
commit 58ecf62240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 520 additions and 305 deletions

View File

@ -791,6 +791,8 @@ class FieldInfo {
case FieldType.Checkbox:
case FieldType.Number:
case FieldType.DateTime:
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return true;
default:
return false;

View File

@ -14,7 +14,7 @@ pub fn make_plugins(
) -> Vec<AFPlugin> {
let store_preferences = user_session
.upgrade()
.and_then(|session| Some(session.get_store_preferences()))
.map(|session| session.get_store_preferences())
.unwrap();
let user_plugin = flowy_user::event_map::init(user_session);
let folder_plugin = flowy_folder2::event_map::init(folder_manager);

View File

@ -31,13 +31,12 @@ impl DatabaseLayoutDepsResolver {
match self.database_layout {
DatabaseLayout::Grid => (None, None),
DatabaseLayout::Board => {
if self
if !self
.database
.lock()
.get_fields(None)
.into_iter()
.find(|field| FieldType::from(field.field_type).can_be_group())
.is_none()
.any(|field| FieldType::from(field.field_type).can_be_group())
{
let select_field = self.create_select_field();
(Some(select_field), None)

View File

@ -11,9 +11,10 @@ use flowy_error::FlowyResult;
use crate::entities::{CheckboxFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, CheckboxCellData, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform,
CheckboxCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CheckboxTypeOption {
@ -68,7 +69,7 @@ impl From<CheckboxTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for CheckboxTypeOption {
impl TypeOptionCellDataSerde for CheckboxTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -135,16 +136,32 @@ impl TypeOptionCellDataCompare for CheckboxTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> 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(),
}
let order = cell_data.is_check().cmp(&other_cell_data.is_check());
sort_condition.evaluate_order(order)
}
fn exempt_from_cmp(&self, _: &<Self as TypeOption>::CellData) -> bool {
false
/// Compares two cell data using a specified sort condition and accounts for uninitialized cells.
///
/// This function checks if either `cell_data` or `other_cell_data` is checked (i.e., has the `is_check` property set).
/// If the right cell is checked and the left cell isn't, the function will return `Ordering::Less`. Conversely, if the
/// left cell is checked and the right one isn't, the function will return `Ordering::Greater`. In all other cases, it returns
/// `Ordering::Equal`.
fn apply_cmp_with_uninitialized(
&self,
cell_data: Option<&<Self as TypeOption>::CellData>,
other_cell_data: Option<&<Self as TypeOption>::CellData>,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data, other_cell_data) {
(None, Some(right_cell_data)) if right_cell_data.is_check() => {
sort_condition.evaluate_order(Ordering::Less)
},
(Some(left_cell_data), None) if left_cell_data.is_check() => {
sort_condition.evaluate_order(Ordering::Greater)
},
_ => Ordering::Equal,
}
}
}

View File

@ -9,7 +9,7 @@ use flowy_error::{FlowyError, FlowyResult};
use crate::entities::FieldType;
use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString};
use crate::services::field::CELL_DATA;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
pub const CHECK: &str = "Yes";
pub const UNCHECK: &str = "No";
@ -31,6 +31,12 @@ impl CheckboxCellData {
}
}
impl TypeOptionCellData for CheckboxCellData {
fn is_cell_empty(&self) -> bool {
false
}
}
impl AsRef<[u8]> for CheckboxCellData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()

View File

@ -2,9 +2,10 @@ use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, FieldType, SelectO
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
use crate::services::field::{
SelectOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
SelectOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
use crate::services::sort::SortCondition;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
@ -32,7 +33,7 @@ impl From<ChecklistTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for ChecklistTypeOption {
impl TypeOptionCellDataSerde for ChecklistTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -191,6 +192,7 @@ impl TypeOptionCellDataCompare for ChecklistTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
_sort_condition: SortCondition,
) -> Ordering {
let left = cell_data.percentage_complete();
let right = other_cell_data.percentage_complete();
@ -202,10 +204,6 @@ impl TypeOptionCellDataCompare for ChecklistTypeOption {
Ordering::Equal
}
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.selected_option_ids.is_empty()
}
}
impl TypeOptionTransform for ChecklistTypeOption {}

View File

@ -1,6 +1,6 @@
use crate::entities::FieldType;
use crate::services::cell::{FromCellChangeset, ToCellChangeset};
use crate::services::field::{SelectOption, CELL_DATA};
use crate::services::field::{SelectOption, TypeOptionCellData, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::{internal_error, FlowyResult};
@ -19,6 +19,12 @@ impl ToString for ChecklistCellData {
}
}
impl TypeOptionCellData for ChecklistCellData {
fn is_cell_empty(&self) -> bool {
self.selected_option_ids.is_empty()
}
}
impl ChecklistCellData {
pub fn selected_options(&self) -> Vec<SelectOption> {
self

View File

@ -9,7 +9,8 @@ mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat, TypeOptionCellData,
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat,
TypeOptionCellDataSerde,
};
#[test]

View File

@ -2,9 +2,10 @@ use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, DateCellChangeset, DateCellData, DateCellDataWrapper, DateFormat, TimeFormat,
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use chrono::format::strftime::StrftimeItems;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone};
use chrono_tz::Tz;
@ -80,7 +81,7 @@ impl From<DateTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for DateTypeOption {
impl TypeOptionCellDataSerde for DateTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -306,16 +307,16 @@ impl TypeOptionCellDataCompare for DateTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data.timestamp, other_cell_data.timestamp) {
(Some(left), Some(right)) => left.cmp(&right),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(Some(left), Some(right)) => {
let order = left.cmp(&right);
sort_condition.evaluate_order(order)
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => default_order(),
}
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.timestamp.is_none()
}
}

View File

@ -15,7 +15,7 @@ use crate::entities::{DateCellDataPB, FieldType};
use crate::services::cell::{
CellProtobufBlobParser, DecodedCellData, FromCellChangeset, FromCellString, ToCellChangeset,
};
use crate::services::field::CELL_DATA;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DateCellChangeset {
@ -62,6 +62,12 @@ impl DateCellData {
}
}
impl TypeOptionCellData for DateCellData {
fn is_cell_empty(&self) -> bool {
self.timestamp.is_none()
}
}
impl From<&Cell> for DateCellData {
fn from(cell: &Cell) -> Self {
let timestamp = cell

View File

@ -18,8 +18,9 @@ use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{
NumberCellFormat, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, CELL_DATA,
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, CELL_DATA,
};
use crate::services::sort::SortCondition;
// Number
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -33,6 +34,12 @@ pub struct NumberTypeOption {
#[derive(Clone, Debug, Default)]
pub struct NumberCellData(pub String);
impl TypeOptionCellData for NumberCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for NumberCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
@ -95,7 +102,7 @@ impl From<NumberTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for NumberTypeOption {
impl TypeOptionCellDataSerde for NumberTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -244,27 +251,41 @@ impl TypeOptionCellDataFilter for NumberTypeOption {
}
impl TypeOptionCellDataCompare for NumberTypeOption {
/// Compares two cell data using a specified sort condition.
///
/// The function checks if either `cell_data` or `other_cell_data` is empty (using the `is_empty` method) and:
/// - If both are empty, it returns `Ordering::Equal`.
/// - If only the left cell is empty, it returns `Ordering::Greater`.
/// - If only the right cell is empty, it returns `Ordering::Less`.
/// - If neither is empty, the cell data is converted into `NumberCellFormat` and compared based on the decimal value.
///
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
let left = NumberCellFormat::from_format_str(&cell_data.0, &self.format);
let right = NumberCellFormat::from_format_str(&other_cell_data.0, &self.format);
match (left, right) {
(Ok(left), Ok(right)) => {
return left.decimal().cmp(right.decimal());
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let left = NumberCellFormat::from_format_str(&cell_data.0, &self.format);
let right = NumberCellFormat::from_format_str(&other_cell_data.0, &self.format);
match (left, right) {
(Ok(left), Ok(right)) => {
let order = left.decimal().cmp(right.decimal());
sort_condition.evaluate_order(order)
},
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => Ordering::Equal,
}
},
(Ok(_), Err(_)) => Ordering::Greater,
(Err(_), Ok(_)) => Ordering::Less,
(Err(_), Err(_)) => Ordering::Equal,
}
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.0.is_empty()
}
}
impl std::default::Default for NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();

View File

@ -1,4 +1,4 @@
use std::cmp::{min, Ordering};
use std::cmp::Ordering;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
@ -11,9 +11,10 @@ use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, SelectOptionCellChangeset, SelectOptionIds,
SelectTypeOptionSharedAction, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter,
SelectTypeOptionSharedAction, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde,
};
use crate::services::sort::SortCondition;
// Multiple select
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -47,7 +48,7 @@ impl From<MultiSelectTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for MultiSelectTypeOption {
impl TypeOptionCellDataSerde for MultiSelectTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -136,35 +137,46 @@ impl TypeOptionCellDataFilter for MultiSelectTypeOption {
}
impl TypeOptionCellDataCompare for MultiSelectTypeOption {
/// Orders two cell values to ensure non-empty cells are moved to the front and empty ones to the back.
///
/// This function compares the two provided cell values (`left` and `right`) to determine their
/// relative ordering:
///
/// - If both cells are empty (`None`), they are considered equal.
/// - If the left cell is empty and the right is not, the left cell is ordered to come after the right.
/// - If the right cell is empty and the left is not, the left cell is ordered to come before the right.
/// - If both cells are non-empty, they are ordered based on their names. If there is an additional sort condition,
/// this condition will further evaluate their order.
///
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> 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(),
};
match cell_data.len().cmp(&other_cell_data.len()) {
Ordering::Equal => {
for (left_id, right_id) in cell_data.iter().zip(other_cell_data.iter()) {
let left = self.options.iter().find(|option| &option.id == left_id);
let right = self.options.iter().find(|option| &option.id == right_id);
let order = match (left, right) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(left_option), Some(right_option)) => {
let name_order = left_option.name.cmp(&right_option.name);
sort_condition.evaluate_order(name_order)
},
};
if order.is_ne() {
return order;
}
if order.is_ne() {
return order;
}
}
default_order()
},
order => sort_condition.evaluate_order(order),
}
default_order()
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.is_empty()
}
}

View File

@ -5,7 +5,7 @@ use flowy_error::FlowyResult;
use crate::entities::FieldType;
use crate::services::cell::{DecodedCellData, FromCellString};
use crate::services::field::CELL_DATA;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
pub const SELECTION_IDS_SEPARATOR: &str = ",";
@ -32,6 +32,12 @@ impl SelectOptionIds {
}
}
impl TypeOptionCellData for SelectOptionIds {
fn is_cell_empty(&self) -> bool {
self.is_empty()
}
}
impl FromCellString for SelectOptionIds {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where

View File

@ -13,7 +13,7 @@ use crate::services::field::selection_type_option::type_option_transform::Select
use crate::services::field::{
make_selected_options, CheckboxCellData, MultiSelectTypeOption, SelectOption,
SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, TypeOption,
TypeOptionCellData, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
/// Defines the shared actions used by SingleSelect or Multi-Select.
@ -122,7 +122,8 @@ where
impl<T> CellDataDecoder for T
where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellData,
T:
SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellDataSerde,
{
fn decode_cell(
&self,

View File

@ -1,12 +1,13 @@
use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter,
default_order, SelectOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde,
};
use crate::services::field::{
SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction,
};
use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
@ -46,7 +47,7 @@ impl From<SingleSelectTypeOption> for TypeOptionData {
}
}
impl TypeOptionCellData for SingleSelectTypeOption {
impl TypeOptionCellDataSerde for SingleSelectTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -131,6 +132,7 @@ impl TypeOptionCellDataCompare for SingleSelectTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (
cell_data
@ -140,16 +142,15 @@ impl TypeOptionCellDataCompare for SingleSelectTypeOption {
.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,
(Some(left), Some(right)) => {
let order = left.name.cmp(&right.name);
sort_condition.evaluate_order(order)
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => default_order(),
}
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.is_empty()
}
}
#[cfg(test)]
@ -222,6 +223,6 @@ mod tests {
// delete
let changeset = SelectOptionCellChangeset::from_delete_options(option_ids);
let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1;
assert!(select_option_ids.is_empty());
assert!(select_option_ids.is_cell_empty());
}
}

View File

@ -16,8 +16,9 @@ use crate::services::cell::{
use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionTransform, CELL_DATA,
TypeOptionCellDataSerde, TypeOptionTransform, CELL_DATA,
};
use crate::services::sort::SortCondition;
/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not
/// used yet.
@ -85,7 +86,7 @@ impl TypeOptionTransform for RichTextTypeOption {
}
}
impl TypeOptionCellData for RichTextTypeOption {
impl TypeOptionCellDataSerde for RichTextTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -152,12 +153,17 @@ impl TypeOptionCellDataCompare for RichTextTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
cell_data.0.cmp(&other_cell_data.0)
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.0.trim().is_empty()
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
},
}
}
}
@ -221,6 +227,12 @@ impl std::ops::Deref for StrCellData {
}
}
impl TypeOptionCellData for StrCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for StrCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value("data").unwrap_or_default())

View File

@ -20,6 +20,7 @@ use crate::services::field::{
RichTextTypeOption, SingleSelectTypeOption, TimeFormat, URLTypeOption,
};
use crate::services::filter::FromFilterString;
use crate::services::sort::SortCondition;
pub trait TypeOption {
/// `CellData` represents as the decoded model for current type option. Each of them impl the
@ -32,7 +33,7 @@ pub trait TypeOption {
///
/// Uses `StrCellData` for any `TypeOption` if their cell data is pure `String`.
///
type CellData: ToString + Default + Send + Sync + Clone + Debug + 'static;
type CellData: TypeOptionCellData + ToString + Default + Send + Sync + Clone + Debug + 'static;
/// Represents as the corresponding field type cell changeset.
/// The changeset must implements the `FromCellChangesetString` and the `ToCellChangesetString` trait.
@ -52,8 +53,11 @@ pub trait TypeOption {
/// Represents as the filter configuration for this type option.
type CellFilter: FromFilterString + Send + Sync + 'static;
}
pub trait TypeOptionCellData: TypeOption {
/// This trait providing serialization and deserialization methods for cell data.
///
/// This trait ensures that a type which implements both `TypeOption` and `TypeOptionCellDataSerde` can
/// be converted to and from a corresponding `Protobuf struct`, and can be parsed from an opaque [Cell] structure.
pub trait TypeOptionCellDataSerde: TypeOption {
/// Encode the cell data into corresponding `Protobuf struct`.
/// For example:
/// FieldType::URL => URLCellDataPB
@ -69,6 +73,18 @@ pub trait TypeOptionCellData: TypeOption {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>;
}
/// This trait that provides methods to extend the [TypeOption::CellData] functionalities.
///
pub trait TypeOptionCellData {
/// Checks if the cell content is considered empty.
///
/// Even if a cell is initialized, its content might still be considered empty
/// based on certain criteria. e.g. empty text, date, select option, etc.
fn is_cell_empty(&self) -> bool {
false
}
}
pub trait TypeOptionTransform: TypeOption {
/// Returns true if the current `TypeOption` provides custom type option transformation
fn transformable(&self) -> bool {
@ -127,13 +143,28 @@ pub fn default_order() -> Ordering {
}
pub trait TypeOptionCellDataCompare: TypeOption {
/// Compares the cell contents of two cells that are both not
/// None. However, the cell contents might still be empty
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering;
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool;
/// Compares the two cells where one of the cells is None
fn apply_cmp_with_uninitialized(
&self,
cell_data: Option<&<Self as TypeOption>::CellData>,
other_cell_data: Option<&<Self as TypeOption>::CellData>,
_sort_condition: SortCondition,
) -> Ordering {
match (cell_data, other_cell_data) {
(None, Some(cell_data)) if !cell_data.is_cell_empty() => Ordering::Greater,
(Some(cell_data), None) if !cell_data.is_cell_empty() => Ordering::Less,
_ => Ordering::Equal,
}
}
}
pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(

View File

@ -16,8 +16,8 @@ use crate::services::cell::{
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
SingleSelectTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOption,
SingleSelectTypeOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
};
use crate::services::sort::SortCondition;
@ -48,8 +48,8 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
fn handle_cell_compare(
&self,
left_cell: &Cell,
right_cell: &Cell,
left_cell: Option<&Cell>,
right_cell: Option<&Cell>,
field: &Field,
sort_condition: SortCondition,
) -> Ordering;
@ -105,7 +105,7 @@ where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellData
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
@ -208,7 +208,7 @@ where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellData
+ TypeOptionCellDataSerde
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
@ -241,32 +241,61 @@ where
Ok(cell)
}
/// Compares two cell data values given their optional references, field information, and sorting condition.
///
/// This function is designed to handle the comparison of cells that might not be initialized. The cells are
/// first decoded based on the provided field type, and then compared according to the specified sort condition.
///
/// # Parameters
/// - `left_cell`: An optional reference to the left cell's data.
/// - `right_cell`: An optional reference to the right cell's data.
/// - `field`: A reference to the field information, which includes details about the field type.
/// - `sort_condition`: The condition that dictates the sort order based on the results of the comparison.
///
/// # Returns
/// An `Ordering` indicating:
/// - `Ordering::Equal` if both cells are `None` or if their decoded values are equal.
/// - `Ordering::Less` or `Ordering::Greater` based on the `apply_cmp_with_uninitialized` or `apply_cmp`
/// method results and the specified `sort_condition`.
///
/// # Note
/// - If only one of the cells is `None`, the other cell is decoded, and the comparison is made using
/// the `apply_cmp_with_uninitialized` method.
/// - If both cells are present, they are decoded, and the comparison is made using the `apply_cmp` method.
fn handle_cell_compare(
&self,
left_cell: &Cell,
right_cell: &Cell,
left_cell: Option<&Cell>,
right_cell: Option<&Cell>,
field: &Field,
sort_condition: SortCondition,
) -> Ordering {
let field_type = FieldType::from(field.field_type);
let left = self
.get_decoded_cell_data(left_cell, &field_type, field)
.unwrap_or_default();
let right = self
.get_decoded_cell_data(right_cell, &field_type, field)
.unwrap_or_default();
match (self.exempt_from_cmp(&left), self.exempt_from_cmp(&right)) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = self.apply_cmp(&left, &right);
// The order is calculated by Ascending. So reverse the order if the SortCondition is descending.
match sort_condition {
SortCondition::Ascending => order,
SortCondition::Descending => order.reverse(),
}
match (left_cell, right_cell) {
(None, None) => Ordering::Equal,
(None, Some(right_cell)) => {
let right_cell_data = self
.get_decoded_cell_data(right_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp_with_uninitialized(None, Some(right_cell_data).as_ref(), sort_condition)
},
(Some(left_cell), None) => {
let left_cell_data = self
.get_decoded_cell_data(left_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp_with_uninitialized(Some(left_cell_data).as_ref(), None, sort_condition)
},
(Some(left_cell), Some(right_cell)) => {
let left_cell_data: <T as TypeOption>::CellData = self
.get_decoded_cell_data(left_cell, &field_type, field)
.unwrap_or_default();
let right_cell_data = self
.get_decoded_cell_data(right_cell, &field_type, field)
.unwrap_or_default();
self.apply_cmp(&left_cell_data, &right_cell_data, sort_condition)
},
}
}

View File

@ -1,9 +1,10 @@
use crate::entities::{FieldType, TextFilterPB, URLCellDataPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform, URLCellData,
};
use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
@ -46,7 +47,7 @@ impl From<URLTypeOption> for TypeOptionData {
impl TypeOptionTransform for URLTypeOption {}
impl TypeOptionCellData for URLTypeOption {
impl TypeOptionCellDataSerde for URLTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
@ -123,12 +124,19 @@ impl TypeOptionCellDataCompare for URLTypeOption {
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
cell_data.data.cmp(&other_cell_data.data)
}
fn exempt_from_cmp(&self, cell_data: &<Self as TypeOption>::CellData) -> bool {
cell_data.data.is_empty()
let is_left_empty = cell_data.data.is_empty();
let is_right_empty = other_cell_data.data.is_empty();
match (is_left_empty, is_right_empty) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = cell_data.data.cmp(&other_cell_data.data);
sort_condition.evaluate_order(order)
},
}
}
}

View File

@ -7,7 +7,7 @@ use flowy_error::{internal_error, FlowyResult};
use crate::entities::{FieldType, URLCellDataPB};
use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString};
use crate::services::field::CELL_DATA;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct URLCellData {
@ -28,6 +28,12 @@ impl URLCellData {
}
}
impl TypeOptionCellData for URLCellData {
fn is_cell_empty(&self) -> bool {
self.data.is_empty()
}
}
impl From<&Cell> for URLCellData {
fn from(cell: &Cell) -> Self {
let url = cell.get_str_value("url").unwrap_or_default();

View File

@ -246,46 +246,20 @@ fn cmp_row(
.find(|field_rev| field_rev.id == sort.field_id)
{
None => default_order(),
Some(field_rev) => match (
Some(field_rev) => cmp_cell(
left.cells.get(&sort.field_id),
right.cells.get(&sort.field_id),
) {
(Some(left_cell), Some(right_cell)) => cmp_cell(
left_cell,
right_cell,
field_rev,
field_type,
cell_data_cache,
sort.condition,
),
(Some(_), None) => {
if field_type.is_checkbox() {
match sort.condition {
SortCondition::Ascending => Ordering::Greater,
SortCondition::Descending => Ordering::Less,
}
} else {
Ordering::Less
}
},
(None, Some(_)) => {
if field_type.is_checkbox() {
match sort.condition {
SortCondition::Ascending => Ordering::Less,
SortCondition::Descending => Ordering::Greater,
}
} else {
Ordering::Greater
}
},
_ => default_order(),
},
field_rev,
field_type,
cell_data_cache,
sort.condition,
),
}
}
fn cmp_cell(
left_cell: &Cell,
right_cell: &Cell,
left_cell: Option<&Cell>,
right_cell: Option<&Cell>,
field: &Arc<Field>,
field_type: FieldType,
cell_data_cache: &CellCache,
@ -306,6 +280,7 @@ fn cmp_cell(
},
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum SortEvent {
SortDidChanged,

View File

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use anyhow::bail;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::RowId;
@ -67,6 +69,16 @@ impl SortCondition {
pub fn value(&self) -> i64 {
*self as i64
}
/// Given an [Ordering] resulting from a comparison,
/// reverse it if the sort condition is descending rather than
/// the default ascending
pub fn evaluate_order(&self, order: Ordering) -> Ordering {
match self {
SortCondition::Ascending => order,
SortCondition::Descending => order.reverse(),
}
}
}
impl Default for SortCondition {

View File

@ -22,7 +22,7 @@ async fn grid_filter_checkbox_is_check_test() {
#[tokio::test]
async fn grid_filter_checkbox_is_uncheck_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 3;
let expected = 4;
let row_count = test.row_details.len();
let scripts = vec![
CreateCheckboxFilter {

View File

@ -6,7 +6,7 @@ use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}
#[tokio::test]
async fn grid_filter_checklist_is_incomplete_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 5;
let expected = 6;
let row_count = test.row_details.len();
let scripts = vec![
UpdateChecklistCell {

View File

@ -84,7 +84,7 @@ async fn grid_filter_number_is_less_than_or_equal_test() {
async fn grid_filter_number_is_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 1;
let expected = 2;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::NumberIsEmpty,

View File

@ -11,7 +11,7 @@ async fn grid_filter_multi_select_is_empty_test() {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
},
AssertNumberOfVisibleRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -24,7 +24,7 @@ async fn grid_filter_multi_select_is_not_empty_test() {
condition: SelectOptionConditionPB::OptionIsNotEmpty,
option_ids: vec![],
},
AssertNumberOfVisibleRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 5 },
];
test.run_scripts(scripts).await;
}
@ -39,7 +39,7 @@ async fn grid_filter_multi_select_is_test() {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(0).id, options.remove(0).id],
},
AssertNumberOfVisibleRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 5 },
];
test.run_scripts(scripts).await;
}
@ -54,7 +54,7 @@ async fn grid_filter_multi_select_is_test2() {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(1).id],
},
AssertNumberOfVisibleRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 4 },
];
test.run_scripts(scripts).await;
}
@ -62,7 +62,7 @@ async fn grid_filter_multi_select_is_test2() {
#[tokio::test]
async fn grid_filter_single_select_is_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 2;
let expected = 3;
let row_count = test.row_details.len();
let scripts = vec![
CreateSingleSelectFilter {

View File

@ -213,7 +213,7 @@ async fn grid_filter_delete_test() {
changed: None,
},
AssertFilterCount { count: 0 },
AssertNumberOfVisibleRows { expected: 6 },
AssertNumberOfVisibleRows { expected: 7 },
])
.await;
}

View File

@ -116,7 +116,7 @@ pub fn make_test_grid() -> DatabaseData {
}
}
for i in 0..6 {
for i in 0..7 {
let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields);
match i {
0 => {
@ -166,9 +166,9 @@ pub fn make_test_grid() -> DatabaseData {
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(0))
},
FieldType::MultiSelect => {
row_builder.insert_multi_select_cell(|mut options| vec![options.remove(1)])
},
FieldType::MultiSelect => row_builder.insert_multi_select_cell(|mut options| {
vec![options.remove(1), options.remove(0), options.remove(0)]
}),
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
_ => "".to_owned(),
};
@ -201,7 +201,8 @@ pub fn make_test_grid() -> DatabaseData {
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(1))
},
FieldType::MultiSelect => row_builder
.insert_multi_select_cell(|mut options| vec![options.remove(1), options.remove(1)]),
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
_ => "".to_owned(),
};
@ -218,11 +219,17 @@ pub fn make_test_grid() -> DatabaseData {
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(1))
},
FieldType::MultiSelect => {
row_builder.insert_multi_select_cell(|mut options| vec![options.remove(1)])
},
FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
_ => "".to_owned(),
};
}
},
6 => {
row_builder.insert_text_cell("CB");
},
_ => {},
}

View File

@ -30,10 +30,11 @@ async fn export_csv_test() {
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Updated At,Created At
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,2022/03/14,2022/03/14
,$2,2022/03/14,,"Google,Twitter",Yes,,,2022/03/14,2022/03/14
C,$3,2022/03/14,Completed,Facebook,No,,,2022/03/14,2022/03/14
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,2022/03/14,2022/03/14
DA,$14,2022/11/17,Completed,,No,,,2022/11/17,2022/11/17
AE,,2022/11/13,Planned,,No,,,2022/11/13,2022/11/13
AE,$5,2022/12/25,Planned,,Yes,,,2022/12/25,2022/12/25
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,2022/11/13,2022/11/13
AE,$5,2022/12/25,Planned,Facebook,Yes,,,2022/12/25,2022/12/25
CB,,,,,,,,,
"#;
println!("{}", s);
assert_eq!(s, expected);

View File

@ -1,49 +0,0 @@
use flowy_database2::entities::FieldType;
use flowy_database2::services::sort::SortCondition;
use crate::database::sort_test::script::DatabaseSortTest;
use crate::database::sort_test::script::SortScript::{AssertCellContentOrder, InsertSort};
#[tokio::test]
async fn sort_checkbox_and_then_text_by_descending_test() {
let mut test = DatabaseSortTest::new().await;
let checkbox_field = test.get_first_field(FieldType::Checkbox);
let text_field = test.get_first_field(FieldType::RichText);
let scripts = vec![
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE"],
},
// Insert checkbox sort
InsertSort {
field: checkbox_field.clone(),
condition: SortCondition::Descending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "AE", "C", "DA", "AE"],
},
// Insert text sort
InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "", "AE", "C", "DA"],
},
];
test.run_scripts(scripts).await;
}

View File

@ -1,4 +1,3 @@
mod checkbox_and_text_test;
mod multi_sort_test;
mod script;
mod single_sort_test;

View File

@ -5,46 +5,44 @@ use crate::database::sort_test::script::DatabaseSortTest;
use crate::database::sort_test::script::SortScript::*;
#[tokio::test]
async fn sort_text_with_checkbox_by_ascending_test() {
async fn sort_checkbox_and_then_text_by_descending_test() {
let mut test = DatabaseSortTest::new().await;
let text_field = test.get_first_field(FieldType::RichText).clone();
let checkbox_field = test.get_first_field(FieldType::Checkbox).clone();
let checkbox_field = test.get_first_field(FieldType::Checkbox);
let text_field = test.get_first_field(FieldType::RichText);
let scripts = vec![
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE"],
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"],
},
InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "DA", ""],
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "No", "Yes", "No", "No", "Yes"],
},
];
test.run_scripts(scripts).await;
let scripts = vec![
// Insert checkbox sort
InsertSort {
field: checkbox_field.clone(),
condition: SortCondition::Descending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "Yes", "No", "No", "No", ""],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "DA", ""],
orders: vec!["A", "", "AE", "C", "DA", "AE", "CB"],
},
// Insert text sort
InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"],
orders: vec!["Yes", "Yes", "Yes", "No", "No", "", "No"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "", "AE", "C", "CB", "DA"],
},
];
test.run_scripts(scripts).await;

View File

@ -10,7 +10,7 @@ async fn sort_text_by_ascending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE"],
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
InsertSort {
field: text_field.clone(),
@ -18,7 +18,28 @@ async fn sort_text_by_ascending_test() {
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "DA", ""],
orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_text_by_descending_test() {
let mut test = DatabaseSortTest::new().await;
let text_field = test.get_first_field(FieldType::RichText);
let scripts = vec![
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
InsertSort {
field: text_field.clone(),
condition: SortCondition::Descending,
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["DA", "CB", "C", "AE", "AE", "A", ""],
},
];
test.run_scripts(scripts).await;
@ -29,13 +50,17 @@ async fn sort_change_notification_by_update_text_test() {
let mut test = DatabaseSortTest::new().await;
let text_field = test.get_first_field(FieldType::RichText).clone();
let scripts = vec![
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "DA", ""],
orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""],
},
// Wait the insert task to finish. The cost of time should be less than 200 milliseconds.
Wait { millis: 200 },
@ -49,8 +74,8 @@ async fn sort_change_notification_by_update_text_test() {
text: "E".to_string(),
},
AssertSortChanged {
old_row_orders: vec!["A", "E", "AE", "C", "DA", ""],
new_row_orders: vec!["A", "AE", "C", "DA", "E", ""],
old_row_orders: vec!["A", "E", "AE", "C", "CB", "DA", ""],
new_row_orders: vec!["A", "AE", "C", "CB", "DA", "E", ""],
},
];
test.run_scripts(scripts).await;
@ -73,28 +98,7 @@ async fn sort_text_by_ascending_and_delete_sort_test() {
},
AssertCellContentOrder {
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 = DatabaseSortTest::new().await;
let text_field = test.get_first_field(FieldType::RichText);
let scripts = vec![
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE"],
},
InsertSort {
field: text_field.clone(),
condition: SortCondition::Descending,
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["DA", "C", "AE", "AE", "A", ""],
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
];
test.run_scripts(scripts).await;
@ -107,12 +111,16 @@ async fn sort_checkbox_by_ascending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No"],
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""],
},
InsertSort {
field: checkbox_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["No", "No", "No", "", "Yes", "Yes", "Yes"],
},
];
test.run_scripts(scripts).await;
}
@ -124,7 +132,7 @@ async fn sort_checkbox_by_descending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"],
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""],
},
InsertSort {
field: checkbox_field.clone(),
@ -132,7 +140,7 @@ async fn sort_checkbox_by_descending_test() {
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"],
orders: vec!["Yes", "Yes", "Yes", "No", "No", "No", ""],
},
];
test.run_scripts(scripts).await;
@ -151,6 +159,8 @@ async fn sort_date_by_ascending_test() {
"2022/03/14",
"2022/11/17",
"2022/11/13",
"2022/12/25",
"",
],
},
InsertSort {
@ -165,6 +175,8 @@ async fn sort_date_by_ascending_test() {
"2022/03/14",
"2022/11/13",
"2022/11/17",
"2022/12/25",
"",
],
},
];
@ -185,6 +197,7 @@ async fn sort_date_by_descending_test() {
"2022/11/17",
"2022/11/13",
"2022/12/25",
"",
],
},
InsertSort {
@ -200,12 +213,34 @@ async fn sort_date_by_descending_test() {
"2022/03/14",
"2022/03/14",
"2022/03/14",
"",
],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_number_by_ascending_test() {
let mut test = DatabaseSortTest::new().await;
let number_field = test.get_first_field(FieldType::Number);
let scripts = vec![
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$1", "$2", "$3", "$14", "", "$5", ""],
},
InsertSort {
field: number_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$1", "$2", "$3", "$5", "$14", "", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_number_by_descending_test() {
let mut test = DatabaseSortTest::new().await;
@ -213,7 +248,7 @@ async fn sort_number_by_descending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$1", "$2", "$3", "$14", "", "$5"],
orders: vec!["$1", "$2", "$3", "$14", "", "$5", ""],
},
InsertSort {
field: number_field.clone(),
@ -221,7 +256,28 @@ async fn sort_number_by_descending_test() {
},
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$14", "$5", "$3", "$2", "$1", ""],
orders: vec!["$14", "$5", "$3", "$2", "$1", "", ""],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_single_select_by_ascending_test() {
let mut test = DatabaseSortTest::new().await;
let single_select = test.get_first_field(FieldType::SingleSelect);
let scripts = vec![
AssertCellContentOrder {
field_id: single_select.id.clone(),
orders: vec!["", "", "Completed", "Completed", "Planned", "Planned", ""],
},
InsertSort {
field: single_select.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: single_select.id.clone(),
orders: vec!["Completed", "Completed", "Planned", "Planned", "", "", ""],
},
];
test.run_scripts(scripts).await;
@ -234,7 +290,7 @@ async fn sort_single_select_by_descending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: single_select.id.clone(),
orders: vec!["", "", "Completed", "Completed", "Planned", "Planned"],
orders: vec!["", "", "Completed", "Completed", "Planned", "Planned", ""],
},
InsertSort {
field: single_select.clone(),
@ -242,7 +298,7 @@ async fn sort_single_select_by_descending_test() {
},
AssertCellContentOrder {
field_id: single_select.id.clone(),
orders: vec!["Planned", "Planned", "Completed", "Completed", "", ""],
orders: vec!["Planned", "Planned", "Completed", "Completed", "", "", ""],
},
];
test.run_scripts(scripts).await;
@ -255,7 +311,15 @@ async fn sort_multi_select_by_ascending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: multi_select.id.clone(),
orders: vec!["Google,Facebook", "Google,Twitter", "Facebook", "", "", ""],
orders: vec![
"Google,Facebook",
"Google,Twitter",
"Facebook,Google,Twitter",
"",
"Facebook,Twitter",
"Facebook",
"",
],
},
InsertSort {
field: multi_select.clone(),
@ -263,7 +327,52 @@ async fn sort_multi_select_by_ascending_test() {
},
AssertCellContentOrder {
field_id: multi_select.id.clone(),
orders: vec!["Facebook", "Google,Facebook", "Google,Twitter", "", "", ""],
orders: vec![
"Facebook",
"Facebook,Twitter",
"Google,Facebook",
"Google,Twitter",
"Facebook,Google,Twitter",
"",
"",
],
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn sort_multi_select_by_descending_test() {
let mut test = DatabaseSortTest::new().await;
let multi_select = test.get_first_field(FieldType::MultiSelect);
let scripts = vec![
AssertCellContentOrder {
field_id: multi_select.id.clone(),
orders: vec![
"Google,Facebook",
"Google,Twitter",
"Facebook,Google,Twitter",
"",
"Facebook,Twitter",
"Facebook",
"",
],
},
InsertSort {
field: multi_select.clone(),
condition: SortCondition::Descending,
},
AssertCellContentOrder {
field_id: multi_select.id.clone(),
orders: vec![
"Facebook,Google,Twitter",
"Google,Twitter",
"Google,Facebook",
"Facebook,Twitter",
"Facebook",
"",
"",
],
},
];
test.run_scripts(scripts).await;

View File

@ -21,9 +21,9 @@ impl std::convert::From<ViewIconTypePB> for IconType {
}
}
impl Into<ViewIconTypePB> for IconType {
fn into(self) -> ViewIconTypePB {
match self {
impl From<IconType> for ViewIconTypePB {
fn from(val: IconType) -> Self {
match val {
IconType::Emoji => ViewIconTypePB::Emoji,
IconType::Url => ViewIconTypePB::Url,
IconType::Icon => ViewIconTypePB::Icon,
@ -48,11 +48,11 @@ impl std::convert::From<ViewIconPB> for ViewIcon {
}
}
impl Into<ViewIconPB> for ViewIcon {
fn into(self) -> ViewIconPB {
impl From<ViewIcon> for ViewIconPB {
fn from(val: ViewIcon) -> Self {
ViewIconPB {
ty: self.ty.into(),
value: self.value,
ty: val.ty.into(),
value: val.value,
}
}
}

View File

@ -45,7 +45,7 @@ pub struct FlowyCoreTest {
impl Default for FlowyCoreTest {
fn default() -> Self {
let temp_dir = PathBuf::from(temp_dir()).join(nanoid!(6));
let temp_dir = temp_dir().join(nanoid!(6));
std::fs::create_dir_all(&temp_dir).unwrap();
Self::new_with_user_data_path(temp_dir, nanoid!(6))
}
@ -57,7 +57,7 @@ impl FlowyCoreTest {
}
pub fn new_with_user_data_path(path: PathBuf, name: String) -> Self {
let config = AppFlowyCoreConfig::new(path.clone().to_str().unwrap(), name).log_filter(
let config = AppFlowyCoreConfig::new(path.to_str().unwrap(), name).log_filter(
"info",
vec!["flowy_test".to_string(), "lib_dispatch".to_string()],
);

View File

@ -102,7 +102,7 @@ async fn update_view_subscription_test() {
let cloned_test = test.clone();
let view = workspace.views.pop().unwrap();
assert_eq!(view.is_favorite, false);
assert!(!view.is_favorite);
let update_view_id = view.id.clone();
tokio::spawn(async move {
@ -123,5 +123,5 @@ async fn update_view_subscription_test() {
let expected_view = update.update_child_views.first().unwrap();
assert_eq!(expected_view.id, view.id);
assert_eq!(expected_view.name, "hello world".to_string());
assert_eq!(expected_view.is_favorite, true);
assert!(expected_view.is_favorite);
}

View File

@ -17,7 +17,7 @@ use crate::{errors::FlowyError, services::UserSession};
pub fn init(user_session: Weak<UserSession>) -> AFPlugin {
let store_preferences = user_session
.upgrade()
.and_then(|session| Some(session.get_store_preferences()))
.map(|session| session.get_store_preferences())
.unwrap();
AFPlugin::new()
.name("Flowy-User")