feat: migrate flowy-database (#2373)

* feat: add flowy-database2

* chore: config type option data

* chore: impl type option

* feat: config group

* fix: group compile

* feat: add sort

* chore: setting

* chore: insert with specific type

* chore: custom group

* chore: rename any map

* chore: use group setting

* chore: update

* chore: open database event

* chore: update database editor

* chore: update

* chore: update view editor

* chore: update

* chore: update view editor

* chore: sort feat

* chore: update handler

* chore: update

* chore: config handler event

* feat: impl handlers

* feat: impl handlers

* chore: layout setting

* feat: impl handlers

* chore: remove flowy-folder ref

* chore: integrate flowy-database2

* feat: get cell

* chore: create database with data

* chore: create view

* chore: fix dart compile

* fix: some bugs

* chore: update

* chore: merge develop

* chore: fix warning

* chore: integrate rocksdb

* fix: rocksdb compile errros

* fix: update cell

* chore: update the bundle identifier

* fix: create row

* fix: switch to field

* fix: duplicate grid

* test: migrate tests

* test: migrate tests

* test: update test

* test: migrate tests

* chore: add patch
This commit is contained in:
Nathan.fooo
2023-04-28 14:08:53 +08:00
committed by GitHub
parent 243f062d4f
commit 32bd0ffca2
316 changed files with 24152 additions and 837 deletions

View File

@ -0,0 +1,137 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::services::setting::{CalendarLayout, CalendarLayoutSetting};
#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf)]
pub struct CalendarLayoutSettingPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub layout_ty: CalendarLayoutPB,
#[pb(index = 3)]
pub first_day_of_week: i32,
#[pb(index = 4)]
pub show_weekends: bool,
#[pb(index = 5)]
pub show_week_numbers: bool,
}
impl std::convert::From<CalendarLayoutSettingPB> for CalendarLayoutSetting {
fn from(pb: CalendarLayoutSettingPB) -> Self {
CalendarLayoutSetting {
layout_ty: pb.layout_ty.into(),
first_day_of_week: pb.first_day_of_week,
show_weekends: pb.show_weekends,
show_week_numbers: pb.show_week_numbers,
field_id: pb.field_id,
}
}
}
impl std::convert::From<CalendarLayoutSetting> for CalendarLayoutSettingPB {
fn from(params: CalendarLayoutSetting) -> Self {
CalendarLayoutSettingPB {
field_id: params.field_id,
layout_ty: params.layout_ty.into(),
first_day_of_week: params.first_day_of_week,
show_weekends: params.show_weekends,
show_week_numbers: params.show_week_numbers,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf_Enum)]
#[repr(u8)]
pub enum CalendarLayoutPB {
#[default]
MonthLayout = 0,
WeekLayout = 1,
DayLayout = 2,
}
impl std::convert::From<CalendarLayoutPB> for CalendarLayout {
fn from(pb: CalendarLayoutPB) -> Self {
match pb {
CalendarLayoutPB::MonthLayout => CalendarLayout::Month,
CalendarLayoutPB::WeekLayout => CalendarLayout::Week,
CalendarLayoutPB::DayLayout => CalendarLayout::Day,
}
}
}
impl std::convert::From<CalendarLayout> for CalendarLayoutPB {
fn from(layout: CalendarLayout) -> Self {
match layout {
CalendarLayout::Month => CalendarLayoutPB::MonthLayout,
CalendarLayout::Week => CalendarLayoutPB::WeekLayout,
CalendarLayout::Day => CalendarLayoutPB::DayLayout,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CalendarEventRequestPB {
#[pb(index = 1)]
pub view_id: String,
// Currently, requesting the events within the specified month
// is not supported
#[pb(index = 2)]
pub month: String,
}
#[derive(Debug, Clone, Default)]
pub struct CalendarEventRequestParams {
pub view_id: String,
pub month: String,
}
impl TryInto<CalendarEventRequestParams> for CalendarEventRequestPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CalendarEventRequestParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
Ok(CalendarEventRequestParams {
view_id: view_id.0,
month: self.month,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CalendarEventPB {
#[pb(index = 1)]
pub row_id: i64,
#[pb(index = 2)]
pub title_field_id: String,
#[pb(index = 3)]
pub title: String,
#[pb(index = 4)]
pub timestamp: i64,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedCalendarEventPB {
#[pb(index = 1)]
pub items: Vec<CalendarEventPB>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveCalendarEventPB {
#[pb(index = 1)]
pub row_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub timestamp: i64,
}

View File

@ -0,0 +1,166 @@
use collab_database::rows::RowId;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::FieldType;
#[derive(ProtoBuf, Default)]
pub struct CreateSelectOptionPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub view_id: String,
#[pb(index = 3)]
pub option_name: String,
}
pub struct CreateSelectOptionParams {
pub field_id: String,
pub view_id: String,
pub option_name: String,
}
impl TryInto<CreateSelectOptionParams> for CreateSelectOptionPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CreateSelectOptionParams, Self::Error> {
let option_name =
NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?;
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(CreateSelectOptionParams {
field_id: field_id.0,
option_name: option_name.0,
view_id: view_id.0,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CellIdPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub row_id: i64,
}
/// Represents as the cell identifier. It's used to locate the cell in corresponding
/// view's row with the field id.
pub struct CellIdParams {
pub view_id: String,
pub field_id: String,
pub row_id: RowId,
}
impl TryInto<CellIdParams> for CellIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CellIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(CellIdParams {
view_id: view_id.0,
field_id: field_id.0,
row_id: RowId::from(self.row_id),
})
}
}
/// Represents as the data of the cell.
#[derive(Debug, Default, ProtoBuf)]
pub struct CellPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub row_id: i64,
/// Encoded the data using the helper struct `CellProtobufBlob`.
/// Check out the `CellProtobufBlob` for more information.
#[pb(index = 3)]
pub data: Vec<u8>,
/// the field_type will be None if the field with field_id is not found
#[pb(index = 4, one_of)]
pub field_type: Option<FieldType>,
}
impl CellPB {
pub fn new(field_id: &str, row_id: i64, field_type: FieldType, data: Vec<u8>) -> Self {
Self {
field_id: field_id.to_owned(),
row_id,
data,
field_type: Some(field_type),
}
}
pub fn empty(field_id: &str, row_id: i64) -> Self {
Self {
field_id: field_id.to_owned(),
row_id,
data: vec![],
field_type: None,
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedCellPB {
#[pb(index = 1)]
pub items: Vec<CellPB>,
}
impl std::ops::Deref for RepeatedCellPB {
type Target = Vec<CellPB>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl std::ops::DerefMut for RepeatedCellPB {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}
impl std::convert::From<Vec<CellPB>> for RepeatedCellPB {
fn from(items: Vec<CellPB>) -> Self {
Self { items }
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CellChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub row_id: i64,
#[pb(index = 3)]
pub field_id: String,
#[pb(index = 4)]
pub cell_changeset: String,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CellChangesetNotifyPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub row_id: i64,
#[pb(index = 3)]
pub field_id: String,
}

View File

@ -0,0 +1,226 @@
use collab_database::rows::RowId;
use collab_database::user::DatabaseRecord;
use collab_database::views::DatabaseLayout;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB};
/// [DatabasePB] describes how many fields and blocks the grid has
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabasePB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub fields: Vec<FieldIdPB>,
#[pb(index = 3)]
pub rows: Vec<RowPB>,
}
#[derive(ProtoBuf, Default)]
pub struct CreateDatabasePayloadPB {
#[pb(index = 1)]
pub name: String,
}
#[derive(Clone, ProtoBuf, Default, Debug)]
pub struct DatabaseViewIdPB {
#[pb(index = 1)]
pub value: String,
}
impl AsRef<str> for DatabaseViewIdPB {
fn as_ref(&self) -> &str {
&self.value
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveFieldPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub from_index: i32,
#[pb(index = 4)]
pub to_index: i32,
}
#[derive(Clone)]
pub struct MoveFieldParams {
pub view_id: String,
pub field_id: String,
pub from_index: i32,
pub to_index: i32,
}
impl TryInto<MoveFieldParams> for MoveFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?;
Ok(MoveFieldParams {
view_id: view_id.0,
field_id: item_id.0,
from_index: self.from_index,
to_index: self.to_index,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: i64,
#[pb(index = 3)]
pub to_row_id: i64,
}
pub struct MoveRowParams {
pub view_id: String,
pub from_row_id: RowId,
pub to_row_id: RowId,
}
impl TryInto<MoveRowParams> for MoveRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
Ok(MoveRowParams {
view_id: view_id.0,
from_row_id: RowId::from(self.from_row_id),
to_row_id: RowId::from(self.to_row_id),
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveGroupRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: i64,
#[pb(index = 3)]
pub to_group_id: String,
#[pb(index = 4, one_of)]
pub to_row_id: Option<i64>,
}
pub struct MoveGroupRowParams {
pub view_id: String,
pub from_row_id: RowId,
pub to_group_id: String,
pub to_row_id: Option<RowId>,
}
impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveGroupRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let to_group_id =
NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
Ok(MoveGroupRowParams {
view_id: view_id.0,
to_group_id: to_group_id.0,
from_row_id: RowId::from(self.from_row_id),
to_row_id: self.to_row_id.map(RowId::from),
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct DatabaseDescriptionPB {
#[pb(index = 1)]
pub name: String,
#[pb(index = 2)]
pub database_id: String,
}
impl From<DatabaseRecord> for DatabaseDescriptionPB {
fn from(data: DatabaseRecord) -> Self {
Self {
name: data.name,
database_id: data.database_id,
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedDatabaseDescriptionPB {
#[pb(index = 1)]
pub items: Vec<DatabaseDescriptionPB>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabaseGroupIdPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub group_id: String,
}
pub struct DatabaseGroupIdParams {
pub view_id: String,
pub group_id: String,
}
impl TryInto<DatabaseGroupIdParams> for DatabaseGroupIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DatabaseGroupIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
Ok(DatabaseGroupIdParams {
view_id: view_id.0,
group_id: group_id.0,
})
}
}
#[derive(Clone, ProtoBuf, Default, Debug)]
pub struct DatabaseLayoutIdPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub layout: DatabaseLayoutPB,
}
#[derive(Clone, Debug)]
pub struct DatabaseLayoutId {
pub view_id: String,
pub layout: DatabaseLayout,
}
impl TryInto<DatabaseLayoutId> for DatabaseLayoutIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DatabaseLayoutId, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let layout = self.layout.into();
Ok(DatabaseLayoutId {
view_id: view_id.0,
layout,
})
}
}

View File

@ -0,0 +1,674 @@
#![allow(clippy::upper_case_acronyms)]
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::views::FieldOrder;
use serde_repr::*;
use strum_macros::{EnumCount as EnumCountMacro, EnumIter};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::impl_into_field_type;
/// [FieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc.
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct FieldPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub name: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub visibility: bool,
#[pb(index = 5)]
pub width: i32,
#[pb(index = 6)]
pub is_primary: bool,
}
impl std::convert::From<Field> for FieldPB {
fn from(field: Field) -> Self {
Self {
id: field.id,
name: field.name,
field_type: FieldType::from(field.field_type),
visibility: field.visibility,
width: field.width as i32,
is_primary: field.is_primary,
}
}
}
/// [FieldIdPB] id of the [Field]
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct FieldIdPB {
#[pb(index = 1)]
pub field_id: String,
}
impl std::convert::From<&str> for FieldIdPB {
fn from(s: &str) -> Self {
FieldIdPB {
field_id: s.to_owned(),
}
}
}
impl std::convert::From<String> for FieldIdPB {
fn from(s: String) -> Self {
FieldIdPB { field_id: s }
}
}
impl From<FieldOrder> for FieldIdPB {
fn from(field_order: FieldOrder) -> Self {
Self {
field_id: field_order.id,
}
}
}
impl std::convert::From<&Arc<Field>> for FieldIdPB {
fn from(field_rev: &Arc<Field>) -> Self {
Self {
field_id: field_rev.id.clone(),
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabaseFieldChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub inserted_fields: Vec<IndexFieldPB>,
#[pb(index = 3)]
pub deleted_fields: Vec<FieldIdPB>,
#[pb(index = 4)]
pub updated_fields: Vec<FieldPB>,
}
impl DatabaseFieldChangesetPB {
pub fn insert(database_id: &str, inserted_fields: Vec<IndexFieldPB>) -> Self {
Self {
view_id: database_id.to_owned(),
inserted_fields,
deleted_fields: vec![],
updated_fields: vec![],
}
}
pub fn delete(database_id: &str, deleted_fields: Vec<FieldIdPB>) -> Self {
Self {
view_id: database_id.to_string(),
inserted_fields: vec![],
deleted_fields,
updated_fields: vec![],
}
}
pub fn update(database_id: &str, updated_fields: Vec<FieldPB>) -> Self {
Self {
view_id: database_id.to_string(),
inserted_fields: vec![],
deleted_fields: vec![],
updated_fields,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct IndexFieldPB {
#[pb(index = 1)]
pub field: FieldPB,
#[pb(index = 2)]
pub index: i32,
}
impl IndexFieldPB {
pub fn from_field(field: Field, index: usize) -> Self {
Self {
field: FieldPB::from(field),
index: index as i32,
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct CreateFieldPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
#[pb(index = 3, one_of)]
pub type_option_data: Option<Vec<u8>>,
}
#[derive(Clone)]
pub struct CreateFieldParams {
pub view_id: String,
pub field_type: FieldType,
pub type_option_data: Option<Vec<u8>>,
}
impl TryInto<CreateFieldParams> for CreateFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CreateFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
Ok(CreateFieldParams {
view_id: view_id.0,
field_type: self.field_type,
type_option_data: self.type_option_data,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct UpdateFieldTypePayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub create_if_not_exist: bool,
}
pub struct EditFieldParams {
pub view_id: String,
pub field_id: String,
pub field_type: FieldType,
}
impl TryInto<EditFieldParams> for UpdateFieldTypePayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<EditFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(EditFieldParams {
view_id: view_id.0,
field_id: field_id.0,
field_type: self.field_type,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct TypeOptionPathPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
}
pub struct TypeOptionPathParams {
pub view_id: String,
pub field_id: String,
pub field_type: FieldType,
}
impl TryInto<TypeOptionPathParams> for TypeOptionPathPB {
type Error = ErrorCode;
fn try_into(self) -> Result<TypeOptionPathParams, Self::Error> {
let database_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(TypeOptionPathParams {
view_id: database_id.0,
field_id: field_id.0,
field_type: self.field_type,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct TypeOptionPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field: FieldPB,
#[pb(index = 3)]
pub type_option_data: Vec<u8>,
}
/// Collection of the [FieldPB]
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedFieldPB {
#[pb(index = 1)]
pub items: Vec<FieldPB>,
}
impl std::ops::Deref for RepeatedFieldPB {
type Target = Vec<FieldPB>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl std::ops::DerefMut for RepeatedFieldPB {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}
impl std::convert::From<Vec<FieldPB>> for RepeatedFieldPB {
fn from(items: Vec<FieldPB>) -> Self {
Self { items }
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedFieldIdPB {
#[pb(index = 1)]
pub items: Vec<FieldIdPB>,
}
impl std::ops::Deref for RepeatedFieldIdPB {
type Target = Vec<FieldIdPB>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl std::convert::From<Vec<FieldIdPB>> for RepeatedFieldIdPB {
fn from(items: Vec<FieldIdPB>) -> Self {
RepeatedFieldIdPB { items }
}
}
impl std::convert::From<String> for RepeatedFieldIdPB {
fn from(s: String) -> Self {
RepeatedFieldIdPB {
items: vec![FieldIdPB::from(s)],
}
}
}
/// [TypeOptionChangesetPB] is used to update the type-option data.
#[derive(ProtoBuf, Default)]
pub struct TypeOptionChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
/// Check out [TypeOptionPB] for more details.
#[pb(index = 3)]
pub type_option_data: Vec<u8>,
}
#[derive(Clone)]
pub struct TypeOptionChangesetParams {
pub view_id: String,
pub field_id: String,
pub type_option_data: Vec<u8>,
}
impl TryInto<TypeOptionChangesetParams> for TypeOptionChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<TypeOptionChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let _ = NotEmptyStr::parse(self.field_id.clone()).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(TypeOptionChangesetParams {
view_id: view_id.0,
field_id: self.field_id,
type_option_data: self.type_option_data,
})
}
}
#[derive(ProtoBuf, Default)]
pub struct GetFieldPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2, one_of)]
pub field_ids: Option<RepeatedFieldIdPB>,
}
pub struct GetFieldParams {
pub view_id: String,
pub field_ids: Option<Vec<String>>,
}
impl TryInto<GetFieldParams> for GetFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<GetFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_ids = self.field_ids.map(|repeated| {
repeated
.items
.into_iter()
.map(|item| item.field_id)
.collect::<Vec<String>>()
});
Ok(GetFieldParams {
view_id: view_id.0,
field_ids,
})
}
}
/// [FieldChangesetPB] is used to modify the corresponding field. It defines which properties of
/// the field can be modified.
///
/// Pass in None if you don't want to modify a property
/// Pass in Some(Value) if you want to modify a property
///
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct FieldChangesetPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub view_id: String,
#[pb(index = 3, one_of)]
pub name: Option<String>,
#[pb(index = 4, one_of)]
pub desc: Option<String>,
#[pb(index = 5, one_of)]
pub field_type: Option<FieldType>,
#[pb(index = 6, one_of)]
pub frozen: Option<bool>,
#[pb(index = 7, one_of)]
pub visibility: Option<bool>,
#[pb(index = 8, one_of)]
pub width: Option<i32>,
// #[pb(index = 9, one_of)]
// pub type_option_data: Option<Vec<u8>>,
}
impl TryInto<FieldChangesetParams> for FieldChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<FieldChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
let field_type = self.field_type.map(FieldType::from);
// if let Some(type_option_data) = self.type_option_data.as_ref() {
// if type_option_data.is_empty() {
// return Err(ErrorCode::TypeOptionDataIsEmpty);
// }
// }
Ok(FieldChangesetParams {
field_id: field_id.0,
view_id: view_id.0,
name: self.name,
desc: self.desc,
field_type,
frozen: self.frozen,
visibility: self.visibility,
width: self.width,
// type_option_data: self.type_option_data,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct FieldChangesetParams {
pub field_id: String,
pub view_id: String,
pub name: Option<String>,
pub desc: Option<String>,
pub field_type: Option<FieldType>,
pub frozen: Option<bool>,
pub visibility: Option<bool>,
pub width: Option<i32>,
// pub type_option_data: Option<Vec<u8>>,
}
/// Certain field types have user-defined options such as color, date format, number format,
/// or a list of values for a multi-select list. These options are defined within a specialization
/// of the FieldTypeOption class.
///
/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype)
/// for more information.
///
/// The order of the enum can't be changed. If you want to add a new type,
/// it would be better to append it to the end of the list.
#[derive(
Debug,
Clone,
PartialEq,
Hash,
Eq,
ProtoBuf_Enum,
EnumCountMacro,
EnumIter,
Serialize_repr,
Deserialize_repr,
)]
#[repr(u8)]
pub enum FieldType {
RichText = 0,
Number = 1,
DateTime = 2,
SingleSelect = 3,
MultiSelect = 4,
Checkbox = 5,
URL = 6,
Checklist = 7,
}
pub const RICH_TEXT_FIELD: FieldType = FieldType::RichText;
pub const NUMBER_FIELD: FieldType = FieldType::Number;
pub const DATE_FIELD: FieldType = FieldType::DateTime;
pub const SINGLE_SELECT_FIELD: FieldType = FieldType::SingleSelect;
pub const MULTI_SELECT_FIELD: FieldType = FieldType::MultiSelect;
pub const CHECKBOX_FIELD: FieldType = FieldType::Checkbox;
pub const URL_FIELD: FieldType = FieldType::URL;
pub const CHECKLIST_FIELD: FieldType = FieldType::Checklist;
impl std::default::Default for FieldType {
fn default() -> Self {
FieldType::RichText
}
}
impl Display for FieldType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let value: i64 = self.clone().into();
f.write_fmt(format_args!("{}", value))
}
}
impl AsRef<FieldType> for FieldType {
fn as_ref(&self) -> &FieldType {
self
}
}
impl From<&FieldType> for FieldType {
fn from(field_type: &FieldType) -> Self {
field_type.clone()
}
}
impl FieldType {
pub fn default_cell_width(&self) -> i32 {
match self {
FieldType::DateTime => 180,
_ => 150,
}
}
pub fn default_name(&self) -> String {
let s = match self {
FieldType::RichText => "Text",
FieldType::Number => "Number",
FieldType::DateTime => "Date",
FieldType::SingleSelect => "Single Select",
FieldType::MultiSelect => "Multi Select",
FieldType::Checkbox => "Checkbox",
FieldType::URL => "URL",
FieldType::Checklist => "Checklist",
};
s.to_string()
}
pub fn is_number(&self) -> bool {
self == &NUMBER_FIELD
}
pub fn is_text(&self) -> bool {
self == &RICH_TEXT_FIELD
}
pub fn is_checkbox(&self) -> bool {
self == &CHECKBOX_FIELD
}
pub fn is_date(&self) -> bool {
self == &DATE_FIELD
}
pub fn is_single_select(&self) -> bool {
self == &SINGLE_SELECT_FIELD
}
pub fn is_multi_select(&self) -> bool {
self == &MULTI_SELECT_FIELD
}
pub fn is_url(&self) -> bool {
self == &URL_FIELD
}
pub fn is_select_option(&self) -> bool {
self == &MULTI_SELECT_FIELD || self == &SINGLE_SELECT_FIELD
}
pub fn is_check_list(&self) -> bool {
self == &CHECKLIST_FIELD
}
pub fn can_be_group(&self) -> bool {
self.is_select_option() || self.is_checkbox() || self.is_url()
}
}
impl_into_field_type!(i64);
impl_into_field_type!(u8);
impl From<FieldType> for i64 {
fn from(ty: FieldType) -> Self {
match ty {
FieldType::RichText => 0,
FieldType::Number => 1,
FieldType::DateTime => 2,
FieldType::SingleSelect => 3,
FieldType::MultiSelect => 4,
FieldType::Checkbox => 5,
FieldType::URL => 6,
FieldType::Checklist => 7,
}
}
}
impl From<&FieldType> for i64 {
fn from(ty: &FieldType) -> Self {
ty.clone() as i64
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DuplicateFieldPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub view_id: String,
}
// #[derive(Debug, Clone, Default, ProtoBuf)]
// pub struct GridFieldIdentifierPayloadPB {
// #[pb(index = 1)]
// pub field_id: String,
//
// #[pb(index = 2)]
// pub view_id: String,
// }
impl TryInto<FieldIdParams> for DuplicateFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<FieldIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(FieldIdParams {
view_id: view_id.0,
field_id: field_id.0,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DeleteFieldPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub view_id: String,
}
impl TryInto<FieldIdParams> for DeleteFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<FieldIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(FieldIdParams {
view_id: view_id.0,
field_id: field_id.0,
})
}
}
pub struct FieldIdParams {
pub field_id: String,
pub view_id: String,
}

View File

@ -0,0 +1,61 @@
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CheckboxFilterPB {
#[pb(index = 1)]
pub condition: CheckboxFilterConditionPB,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum CheckboxFilterConditionPB {
IsChecked = 0,
IsUnChecked = 1,
}
impl std::convert::From<CheckboxFilterConditionPB> for u32 {
fn from(value: CheckboxFilterConditionPB) -> Self {
value as u32
}
}
impl std::default::Default for CheckboxFilterConditionPB {
fn default() -> Self {
CheckboxFilterConditionPB::IsChecked
}
}
impl std::convert::TryFrom<u8> for CheckboxFilterConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(CheckboxFilterConditionPB::IsChecked),
1 => Ok(CheckboxFilterConditionPB::IsUnChecked),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for CheckboxFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(CheckboxFilterConditionPB::IsChecked),
}
}
}
impl std::convert::From<&Filter> for CheckboxFilterPB {
fn from(filter: &Filter) -> Self {
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(CheckboxFilterConditionPB::IsChecked),
}
}
}

View File

@ -0,0 +1,61 @@
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct ChecklistFilterPB {
#[pb(index = 1)]
pub condition: ChecklistFilterConditionPB,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum ChecklistFilterConditionPB {
IsComplete = 0,
IsIncomplete = 1,
}
impl std::convert::From<ChecklistFilterConditionPB> for u32 {
fn from(value: ChecklistFilterConditionPB) -> Self {
value as u32
}
}
impl std::default::Default for ChecklistFilterConditionPB {
fn default() -> Self {
ChecklistFilterConditionPB::IsIncomplete
}
}
impl std::convert::TryFrom<u8> for ChecklistFilterConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(ChecklistFilterConditionPB::IsComplete),
1 => Ok(ChecklistFilterConditionPB::IsIncomplete),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for ChecklistFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
ChecklistFilterPB {
condition: ChecklistFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(ChecklistFilterConditionPB::IsIncomplete),
}
}
}
impl std::convert::From<&Filter> for ChecklistFilterPB {
fn from(filter: &Filter) -> Self {
ChecklistFilterPB {
condition: ChecklistFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(ChecklistFilterConditionPB::IsIncomplete),
}
}
}

View File

@ -0,0 +1,121 @@
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct DateFilterPB {
#[pb(index = 1)]
pub condition: DateFilterConditionPB,
#[pb(index = 2, one_of)]
pub start: Option<i64>,
#[pb(index = 3, one_of)]
pub end: Option<i64>,
#[pb(index = 4, one_of)]
pub timestamp: Option<i64>,
}
#[derive(Deserialize, Serialize, Default, Clone, Debug)]
pub struct DateFilterContentPB {
pub start: Option<i64>,
pub end: Option<i64>,
pub timestamp: Option<i64>,
}
impl ToString for DateFilterContentPB {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
impl FromStr for DateFilterContentPB {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum DateFilterConditionPB {
DateIs = 0,
DateBefore = 1,
DateAfter = 2,
DateOnOrBefore = 3,
DateOnOrAfter = 4,
DateWithIn = 5,
DateIsEmpty = 6,
DateIsNotEmpty = 7,
}
impl std::convert::From<DateFilterConditionPB> for u32 {
fn from(value: DateFilterConditionPB) -> Self {
value as u32
}
}
impl std::default::Default for DateFilterConditionPB {
fn default() -> Self {
DateFilterConditionPB::DateIs
}
}
impl std::convert::TryFrom<u8> for DateFilterConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(DateFilterConditionPB::DateIs),
1 => Ok(DateFilterConditionPB::DateBefore),
2 => Ok(DateFilterConditionPB::DateAfter),
3 => Ok(DateFilterConditionPB::DateOnOrBefore),
4 => Ok(DateFilterConditionPB::DateOnOrAfter),
5 => Ok(DateFilterConditionPB::DateWithIn),
6 => Ok(DateFilterConditionPB::DateIsEmpty),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for DateFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
let condition = DateFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(DateFilterConditionPB::DateIs);
let mut date_filter = DateFilterPB {
condition,
..Default::default()
};
if let Ok(content) = DateFilterContentPB::from_str(&filter.content) {
date_filter.start = content.start;
date_filter.end = content.end;
date_filter.timestamp = content.timestamp;
};
date_filter
}
}
impl std::convert::From<&Filter> for DateFilterPB {
fn from(filter: &Filter) -> Self {
let condition = DateFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(DateFilterConditionPB::DateIs);
let mut date_filter = DateFilterPB {
condition,
..Default::default()
};
if let Ok(content) = DateFilterContentPB::from_str(&filter.content) {
date_filter.start = content.start;
date_filter.end = content.end;
date_filter.timestamp = content.timestamp;
};
date_filter
}
}

View File

@ -0,0 +1,54 @@
use crate::entities::FilterPB;
use flowy_derive::ProtoBuf;
#[derive(Debug, Default, ProtoBuf)]
pub struct FilterChangesetNotificationPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub insert_filters: Vec<FilterPB>,
#[pb(index = 3)]
pub delete_filters: Vec<FilterPB>,
#[pb(index = 4)]
pub update_filters: Vec<UpdatedFilter>,
}
#[derive(Debug, Default, ProtoBuf)]
pub struct UpdatedFilter {
#[pb(index = 1)]
pub filter_id: String,
#[pb(index = 2, one_of)]
pub filter: Option<FilterPB>,
}
impl FilterChangesetNotificationPB {
pub fn from_insert(view_id: &str, filters: Vec<FilterPB>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: filters,
delete_filters: Default::default(),
update_filters: Default::default(),
}
}
pub fn from_delete(view_id: &str, filters: Vec<FilterPB>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: Default::default(),
delete_filters: filters,
update_filters: Default::default(),
}
}
pub fn from_update(view_id: &str, filters: Vec<UpdatedFilter>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: Default::default(),
delete_filters: Default::default(),
update_filters: filters,
}
}
}

View File

@ -0,0 +1,17 @@
mod checkbox_filter;
mod checklist_filter;
mod date_filter;
mod filter_changeset;
mod number_filter;
mod select_option_filter;
mod text_filter;
mod util;
pub use checkbox_filter::*;
pub use checklist_filter::*;
pub use date_filter::*;
pub use filter_changeset::*;
pub use number_filter::*;
pub use select_option_filter::*;
pub use text_filter::*;
pub use util::*;

View File

@ -0,0 +1,76 @@
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct NumberFilterPB {
#[pb(index = 1)]
pub condition: NumberFilterConditionPB,
#[pb(index = 2)]
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum NumberFilterConditionPB {
Equal = 0,
NotEqual = 1,
GreaterThan = 2,
LessThan = 3,
GreaterThanOrEqualTo = 4,
LessThanOrEqualTo = 5,
NumberIsEmpty = 6,
NumberIsNotEmpty = 7,
}
impl std::default::Default for NumberFilterConditionPB {
fn default() -> Self {
NumberFilterConditionPB::Equal
}
}
impl std::convert::From<NumberFilterConditionPB> for u32 {
fn from(value: NumberFilterConditionPB) -> Self {
value as u32
}
}
impl std::convert::TryFrom<u8> for NumberFilterConditionPB {
type Error = ErrorCode;
fn try_from(n: u8) -> Result<Self, Self::Error> {
match n {
0 => Ok(NumberFilterConditionPB::Equal),
1 => Ok(NumberFilterConditionPB::NotEqual),
2 => Ok(NumberFilterConditionPB::GreaterThan),
3 => Ok(NumberFilterConditionPB::LessThan),
4 => Ok(NumberFilterConditionPB::GreaterThanOrEqualTo),
5 => Ok(NumberFilterConditionPB::LessThanOrEqualTo),
6 => Ok(NumberFilterConditionPB::NumberIsEmpty),
7 => Ok(NumberFilterConditionPB::NumberIsNotEmpty),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for NumberFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
NumberFilterPB {
condition: NumberFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(NumberFilterConditionPB::Equal),
content: filter.content.clone(),
}
}
}
impl std::convert::From<&Filter> for NumberFilterPB {
fn from(filter: &Filter) -> Self {
NumberFilterPB {
condition: NumberFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(NumberFilterConditionPB::Equal),
content: filter.content.clone(),
}
}
}

View File

@ -0,0 +1,72 @@
use crate::services::field::SelectOptionIds;
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SelectOptionFilterPB {
#[pb(index = 1)]
pub condition: SelectOptionConditionPB,
#[pb(index = 2)]
pub option_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum SelectOptionConditionPB {
OptionIs = 0,
OptionIsNot = 1,
OptionIsEmpty = 2,
OptionIsNotEmpty = 3,
}
impl std::convert::From<SelectOptionConditionPB> for u32 {
fn from(value: SelectOptionConditionPB) -> Self {
value as u32
}
}
impl std::default::Default for SelectOptionConditionPB {
fn default() -> Self {
SelectOptionConditionPB::OptionIs
}
}
impl std::convert::TryFrom<u8> for SelectOptionConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(SelectOptionConditionPB::OptionIs),
1 => Ok(SelectOptionConditionPB::OptionIsNot),
2 => Ok(SelectOptionConditionPB::OptionIsEmpty),
3 => Ok(SelectOptionConditionPB::OptionIsNotEmpty),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for SelectOptionFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
let ids = SelectOptionIds::from(filter.content.clone());
SelectOptionFilterPB {
condition: SelectOptionConditionPB::try_from(filter.condition as u8)
.unwrap_or(SelectOptionConditionPB::OptionIs),
option_ids: ids.into_inner(),
}
}
}
impl std::convert::From<&Filter> for SelectOptionFilterPB {
fn from(filter: &Filter) -> Self {
let ids = SelectOptionIds::from(filter.content.clone());
SelectOptionFilterPB {
condition: SelectOptionConditionPB::try_from(filter.condition as u8)
.unwrap_or(SelectOptionConditionPB::OptionIs),
option_ids: ids.into_inner(),
}
}
}

View File

@ -0,0 +1,78 @@
use crate::services::filter::{Filter, FromFilterString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TextFilterPB {
#[pb(index = 1)]
pub condition: TextFilterConditionPB,
#[pb(index = 2)]
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum TextFilterConditionPB {
Is = 0,
IsNot = 1,
Contains = 2,
DoesNotContain = 3,
StartsWith = 4,
EndsWith = 5,
TextIsEmpty = 6,
TextIsNotEmpty = 7,
}
impl std::convert::From<TextFilterConditionPB> for u32 {
fn from(value: TextFilterConditionPB) -> Self {
value as u32
}
}
impl std::default::Default for TextFilterConditionPB {
fn default() -> Self {
TextFilterConditionPB::Is
}
}
impl std::convert::TryFrom<u8> for TextFilterConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(TextFilterConditionPB::Is),
1 => Ok(TextFilterConditionPB::IsNot),
2 => Ok(TextFilterConditionPB::Contains),
3 => Ok(TextFilterConditionPB::DoesNotContain),
4 => Ok(TextFilterConditionPB::StartsWith),
5 => Ok(TextFilterConditionPB::EndsWith),
6 => Ok(TextFilterConditionPB::TextIsEmpty),
7 => Ok(TextFilterConditionPB::TextIsNotEmpty),
_ => Err(ErrorCode::InvalidData),
}
}
}
impl FromFilterString for TextFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
TextFilterPB {
condition: TextFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(TextFilterConditionPB::Is),
content: filter.content.clone(),
}
}
}
impl std::convert::From<&Filter> for TextFilterPB {
fn from(filter: &Filter) -> Self {
TextFilterPB {
condition: TextFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(TextFilterConditionPB::Is),
content: filter.content.clone(),
}
}
}

View File

@ -0,0 +1,235 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType,
NumberFilterPB, SelectOptionFilterPB, TextFilterPB,
};
use crate::services::field::SelectOptionIds;
use crate::services::filter::{Filter, FilterType};
use bytes::Bytes;
use collab_database::fields::Field;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use std::convert::TryInto;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct FilterPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub data: Vec<u8>,
}
impl std::convert::From<&Filter> for FilterPB {
fn from(filter: &Filter) -> Self {
let bytes: Bytes = match filter.field_type {
FieldType::RichText => TextFilterPB::from(filter).try_into().unwrap(),
FieldType::Number => NumberFilterPB::from(filter).try_into().unwrap(),
FieldType::DateTime => DateFilterPB::from(filter).try_into().unwrap(),
FieldType::SingleSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
FieldType::MultiSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(),
FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(),
FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(),
};
Self {
id: filter.id.clone(),
field_id: filter.field_id.clone(),
field_type: filter.field_type.clone(),
data: bytes.to_vec(),
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedFilterPB {
#[pb(index = 1)]
pub items: Vec<FilterPB>,
}
impl std::convert::From<Vec<Arc<Filter>>> for RepeatedFilterPB {
fn from(filters: Vec<Arc<Filter>>) -> Self {
RepeatedFilterPB {
items: filters.into_iter().map(|rev| rev.as_ref().into()).collect(),
}
}
}
impl std::convert::From<Vec<FilterPB>> for RepeatedFilterPB {
fn from(items: Vec<FilterPB>) -> Self {
Self { items }
}
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct DeleteFilterPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
#[pb(index = 3)]
pub filter_id: String,
#[pb(index = 4)]
pub view_id: String,
}
impl TryInto<DeleteFilterParams> for DeleteFilterPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DeleteFilterParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let filter_id = NotEmptyStr::parse(self.filter_id)
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
.0;
let filter_type = FilterType {
filter_id: filter_id.clone(),
field_id,
field_type: self.field_type,
};
Ok(DeleteFilterParams {
view_id,
filter_id,
filter_type,
})
}
}
#[derive(Debug)]
pub struct DeleteFilterParams {
pub view_id: String,
pub filter_id: String,
pub filter_type: FilterType,
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct AlterFilterPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
/// Create a new filter if the filter_id is None
#[pb(index = 3, one_of)]
pub filter_id: Option<String>,
#[pb(index = 4)]
pub data: Vec<u8>,
#[pb(index = 5)]
pub view_id: String,
}
impl AlterFilterPayloadPB {
#[allow(dead_code)]
pub fn new<T: TryInto<Bytes, Error = ::protobuf::ProtobufError>>(
view_id: &str,
field: &Field,
data: T,
) -> Self {
let data = data.try_into().unwrap_or_else(|_| Bytes::new());
let field_type = FieldType::from(field.field_type);
Self {
view_id: view_id.to_owned(),
field_id: field.id.clone(),
field_type,
filter_id: None,
data: data.to_vec(),
}
}
}
impl TryInto<AlterFilterParams> for AlterFilterPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<AlterFilterParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let filter_id = match self.filter_id {
None => None,
Some(filter_id) => Some(
NotEmptyStr::parse(filter_id)
.map_err(|_| ErrorCode::FilterIdIsEmpty)?
.0,
),
};
let condition;
let mut content = "".to_string();
let bytes: &[u8] = self.data.as_ref();
match self.field_type {
FieldType::RichText | FieldType::URL => {
let filter = TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = filter.content;
},
FieldType::Checkbox => {
let filter = CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
},
FieldType::Number => {
let filter = NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = filter.content;
},
FieldType::DateTime => {
let filter = DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = DateFilterContentPB {
start: filter.start,
end: filter.end,
timestamp: filter.timestamp,
}
.to_string();
},
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
let filter = SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = SelectOptionIds::from(filter.option_ids).to_string();
},
}
Ok(AlterFilterParams {
view_id,
field_id,
filter_id,
field_type: self.field_type,
condition: condition as i64,
content,
})
}
}
#[derive(Debug)]
pub struct AlterFilterParams {
pub view_id: String,
pub field_id: String,
/// Create a new filter if the filter_id is None
pub filter_id: Option<String>,
pub field_type: FieldType,
pub condition: i64,
pub content: String,
}

View File

@ -0,0 +1,75 @@
use crate::services::group::Group;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct UrlGroupConfigurationPB {
#[pb(index = 1)]
hide_empty: bool,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TextGroupConfigurationPB {
#[pb(index = 1)]
hide_empty: bool,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SelectOptionGroupConfigurationPB {
#[pb(index = 1)]
hide_empty: bool,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct GroupRecordPB {
#[pb(index = 1)]
group_id: String,
#[pb(index = 2)]
visible: bool,
}
impl std::convert::From<Group> for GroupRecordPB {
fn from(rev: Group) -> Self {
Self {
group_id: rev.id,
visible: rev.visible,
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct NumberGroupConfigurationPB {
#[pb(index = 1)]
hide_empty: bool,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct DateGroupConfigurationPB {
#[pb(index = 1)]
pub condition: DateCondition,
#[pb(index = 2)]
hide_empty: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum DateCondition {
Relative = 0,
Day = 1,
Week = 2,
Month = 3,
Year = 4,
}
impl std::default::Default for DateCondition {
fn default() -> Self {
DateCondition::Relative
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CheckboxGroupConfigurationPB {
#[pb(index = 1)]
pub(crate) hide_empty: bool,
}

View File

@ -0,0 +1,185 @@
use std::convert::TryInto;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{FieldType, RowPB};
use crate::services::group::{GroupData, GroupSetting};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct GroupSettingPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub field_id: String,
}
impl std::convert::From<&GroupSetting> for GroupSettingPB {
fn from(rev: &GroupSetting) -> Self {
GroupSettingPB {
id: rev.id.clone(),
field_id: rev.field_id.clone(),
}
}
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedGroupPB {
#[pb(index = 1)]
pub items: Vec<GroupPB>,
}
impl std::ops::Deref for RepeatedGroupPB {
type Target = Vec<GroupPB>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl std::ops::DerefMut for RepeatedGroupPB {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct GroupPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub group_id: String,
#[pb(index = 3)]
pub desc: String,
#[pb(index = 4)]
pub rows: Vec<RowPB>,
#[pb(index = 5)]
pub is_default: bool,
#[pb(index = 6)]
pub is_visible: bool,
}
impl std::convert::From<GroupData> for GroupPB {
fn from(group_data: GroupData) -> Self {
Self {
field_id: group_data.field_id,
group_id: group_data.id,
desc: group_data.name,
rows: group_data.rows.into_iter().map(RowPB::from).collect(),
is_default: group_data.is_default,
is_visible: group_data.is_visible,
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedGroupSettingPB {
#[pb(index = 1)]
pub items: Vec<GroupSettingPB>,
}
impl std::convert::From<Vec<GroupSettingPB>> for RepeatedGroupSettingPB {
fn from(items: Vec<GroupSettingPB>) -> Self {
Self { items }
}
}
impl std::convert::From<Vec<GroupSetting>> for RepeatedGroupSettingPB {
fn from(group_settings: Vec<GroupSetting>) -> Self {
RepeatedGroupSettingPB {
items: group_settings
.iter()
.map(|setting| setting.into())
.collect(),
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct InsertGroupPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
#[pb(index = 3)]
pub view_id: String,
}
impl TryInto<InsertGroupParams> for InsertGroupPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<InsertGroupParams, Self::Error> {
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
.0;
Ok(InsertGroupParams {
field_id,
field_type: self.field_type,
view_id,
})
}
}
pub struct InsertGroupParams {
pub view_id: String,
pub field_id: String,
pub field_type: FieldType,
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct DeleteGroupPayloadPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub group_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub view_id: String,
}
impl TryInto<DeleteGroupParams> for DeleteGroupPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DeleteGroupParams, Self::Error> {
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let group_id = NotEmptyStr::parse(self.group_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
.0;
Ok(DeleteGroupParams {
field_id,
field_type: self.field_type,
group_id,
view_id,
})
}
}
pub struct DeleteGroupParams {
pub view_id: String,
pub field_id: String,
pub group_id: String,
pub field_type: FieldType,
}

View File

@ -0,0 +1,165 @@
use std::fmt::Formatter;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{GroupPB, InsertedRowPB, RowPB};
#[derive(Debug, Default, ProtoBuf)]
pub struct GroupRowsNotificationPB {
#[pb(index = 1)]
pub group_id: String,
#[pb(index = 2, one_of)]
pub group_name: Option<String>,
#[pb(index = 3)]
pub inserted_rows: Vec<InsertedRowPB>,
#[pb(index = 4)]
pub deleted_rows: Vec<i64>,
#[pb(index = 5)]
pub updated_rows: Vec<RowPB>,
}
impl std::fmt::Display for GroupRowsNotificationPB {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for inserted_row in &self.inserted_rows {
f.write_fmt(format_args!(
"Insert: {} row at {:?}",
inserted_row.row.id, inserted_row.index
))?;
}
for deleted_row in &self.deleted_rows {
f.write_fmt(format_args!("Delete: {} row", deleted_row))?;
}
Ok(())
}
}
impl GroupRowsNotificationPB {
pub fn is_empty(&self) -> bool {
self.group_name.is_none()
&& self.inserted_rows.is_empty()
&& self.deleted_rows.is_empty()
&& self.updated_rows.is_empty()
}
pub fn new(group_id: String) -> Self {
Self {
group_id,
..Default::default()
}
}
pub fn name(group_id: String, name: &str) -> Self {
Self {
group_id,
group_name: Some(name.to_owned()),
..Default::default()
}
}
pub fn insert(group_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {
Self {
group_id,
inserted_rows,
..Default::default()
}
}
pub fn delete(group_id: String, deleted_rows: Vec<i64>) -> Self {
Self {
group_id,
deleted_rows,
..Default::default()
}
}
pub fn update(group_id: String, updated_rows: Vec<RowPB>) -> Self {
Self {
group_id,
updated_rows,
..Default::default()
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct MoveGroupPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_group_id: String,
#[pb(index = 3)]
pub to_group_id: String,
}
#[derive(Debug)]
pub struct MoveGroupParams {
pub view_id: String,
pub from_group_id: String,
pub to_group_id: String,
}
impl TryInto<MoveGroupParams> for MoveGroupPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveGroupParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let from_group_id = NotEmptyStr::parse(self.from_group_id)
.map_err(|_| ErrorCode::GroupIdIsEmpty)?
.0;
let to_group_id = NotEmptyStr::parse(self.to_group_id)
.map_err(|_| ErrorCode::GroupIdIsEmpty)?
.0;
Ok(MoveGroupParams {
view_id,
from_group_id,
to_group_id,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct GroupChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub inserted_groups: Vec<InsertedGroupPB>,
#[pb(index = 3)]
pub initial_groups: Vec<GroupPB>,
#[pb(index = 4)]
pub deleted_groups: Vec<String>,
#[pb(index = 5)]
pub update_groups: Vec<GroupPB>,
}
impl GroupChangesetPB {
pub fn is_empty(&self) -> bool {
self.initial_groups.is_empty()
&& self.inserted_groups.is_empty()
&& self.deleted_groups.is_empty()
&& self.update_groups.is_empty()
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct InsertedGroupPB {
#[pb(index = 1)]
pub group: GroupPB,
#[pb(index = 2)]
pub index: i32,
}

View File

@ -0,0 +1,7 @@
mod configuration;
mod group;
mod group_changeset;
pub use configuration::*;
pub use group::*;
pub use group_changeset::*;

View File

@ -0,0 +1,23 @@
#[macro_export]
macro_rules! impl_into_field_type {
($target: ident) => {
impl std::convert::From<$target> for FieldType {
fn from(ty: $target) -> Self {
match ty {
0 => FieldType::RichText,
1 => FieldType::Number,
2 => FieldType::DateTime,
3 => FieldType::SingleSelect,
4 => FieldType::MultiSelect,
5 => FieldType::Checkbox,
6 => FieldType::URL,
7 => FieldType::Checklist,
_ => {
tracing::error!("Can't parser FieldType from value: {}", ty);
FieldType::RichText
},
}
}
}
};
}

View File

@ -0,0 +1,27 @@
mod calendar_entities;
mod cell_entities;
mod database_entities;
mod field_entities;
pub mod filter_entities;
mod group_entities;
pub mod parser;
mod row_entities;
pub mod setting_entities;
mod sort_entities;
mod view_entities;
#[macro_use]
mod macros;
mod type_option_entities;
pub use calendar_entities::*;
pub use cell_entities::*;
pub use database_entities::*;
pub use field_entities::*;
pub use filter_entities::*;
pub use group_entities::*;
pub use row_entities::*;
pub use setting_entities::*;
pub use sort_entities::*;
pub use type_option_entities::*;
pub use view_entities::*;

View File

@ -0,0 +1,17 @@
#[derive(Debug)]
pub struct NotEmptyStr(pub String);
impl NotEmptyStr {
pub fn parse(s: String) -> Result<Self, String> {
if s.trim().is_empty() {
return Err("Input string is empty".to_owned());
}
Ok(Self(s))
}
}
impl AsRef<str> for NotEmptyStr {
fn as_ref(&self) -> &str {
&self.0
}
}

View File

@ -0,0 +1,219 @@
use std::collections::HashMap;
use collab_database::rows::{Row, RowId};
use collab_database::views::RowOrder;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::services::database::{InsertedRow, UpdatedRow};
/// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row.
#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)]
pub struct RowPB {
#[pb(index = 1)]
pub id: i64,
#[pb(index = 2)]
pub height: i32,
}
impl std::convert::From<&Row> for RowPB {
fn from(row: &Row) -> Self {
Self {
id: row.id.into(),
height: row.height,
}
}
}
impl std::convert::From<Row> for RowPB {
fn from(row: Row) -> Self {
Self {
id: row.id.into(),
height: row.height,
}
}
}
impl From<RowOrder> for RowPB {
fn from(data: RowOrder) -> Self {
Self {
id: data.id.into(),
height: data.height,
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct OptionalRowPB {
#[pb(index = 1, one_of)]
pub row: Option<RowPB>,
}
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedRowPB {
#[pb(index = 1)]
pub items: Vec<RowPB>,
}
impl std::convert::From<Vec<RowPB>> for RepeatedRowPB {
fn from(items: Vec<RowPB>) -> Self {
Self { items }
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct InsertedRowPB {
#[pb(index = 1)]
pub row: RowPB,
#[pb(index = 2, one_of)]
pub index: Option<i32>,
#[pb(index = 3)]
pub is_new: bool,
}
impl InsertedRowPB {
pub fn new(row: RowPB) -> Self {
Self {
row,
index: None,
is_new: false,
}
}
pub fn with_index(row: RowPB, index: i32) -> Self {
Self {
row,
index: Some(index),
is_new: false,
}
}
}
impl std::convert::From<RowPB> for InsertedRowPB {
fn from(row: RowPB) -> Self {
Self {
row,
index: None,
is_new: false,
}
}
}
impl std::convert::From<&Row> for InsertedRowPB {
fn from(row: &Row) -> Self {
Self::from(RowPB::from(row))
}
}
impl From<InsertedRow> for InsertedRowPB {
fn from(data: InsertedRow) -> Self {
Self {
row: data.row.into(),
index: data.index,
is_new: data.is_new,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct UpdatedRowPB {
#[pb(index = 1)]
pub row: RowPB,
// represents as the cells that were updated in this row.
#[pb(index = 2)]
pub field_ids: Vec<String>,
}
impl From<UpdatedRow> for UpdatedRowPB {
fn from(data: UpdatedRow) -> Self {
Self {
row: data.row.into(),
field_ids: data.field_ids,
}
}
}
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct RowIdPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub row_id: i64,
}
pub struct RowIdParams {
pub view_id: String,
pub row_id: RowId,
}
impl TryInto<RowIdParams> for RowIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<RowIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
Ok(RowIdParams {
view_id: view_id.0,
row_id: RowId::from(self.row_id),
})
}
}
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct BlockRowIdPB {
#[pb(index = 1)]
pub block_id: String,
#[pb(index = 2)]
pub row_id: String,
}
#[derive(ProtoBuf, Default)]
pub struct CreateRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2, one_of)]
pub start_row_id: Option<i64>,
#[pb(index = 3, one_of)]
pub group_id: Option<String>,
#[pb(index = 4, one_of)]
pub data: Option<RowDataPB>,
}
#[derive(ProtoBuf, Default)]
pub struct RowDataPB {
#[pb(index = 1)]
pub cell_data_by_field_id: HashMap<String, String>,
}
#[derive(Default)]
pub struct CreateRowParams {
pub view_id: String,
pub start_row_id: Option<RowId>,
pub group_id: Option<String>,
pub cell_data_by_field_id: Option<HashMap<String, String>>,
}
impl TryInto<CreateRowParams> for CreateRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<CreateRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
let start_row_id = self.start_row_id.map(RowId::from);
Ok(CreateRowParams {
view_id: view_id.0,
start_row_id,
group_id: self.group_id,
cell_data_by_field_id: self.data.map(|data| data.cell_data_by_field_id),
})
}
}

View File

@ -0,0 +1,213 @@
use std::convert::TryInto;
use collab_database::views::DatabaseLayout;
use strum_macros::EnumIter;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{
AlterFilterParams, AlterFilterPayloadPB, AlterSortParams, AlterSortPayloadPB,
CalendarLayoutSettingPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams,
DeleteGroupPayloadPB, DeleteSortParams, DeleteSortPayloadPB, InsertGroupParams,
InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB,
};
use crate::services::setting::CalendarLayoutSetting;
/// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort.
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct DatabaseViewSettingPB {
#[pb(index = 1)]
pub current_layout: DatabaseLayoutPB,
#[pb(index = 2)]
pub layout_setting: LayoutSettingPB,
#[pb(index = 3)]
pub filters: RepeatedFilterPB,
#[pb(index = 4)]
pub group_settings: RepeatedGroupSettingPB,
#[pb(index = 5)]
pub sorts: RepeatedSortPB,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)]
#[repr(u8)]
pub enum DatabaseLayoutPB {
Grid = 0,
Board = 1,
Calendar = 2,
}
impl std::default::Default for DatabaseLayoutPB {
fn default() -> Self {
DatabaseLayoutPB::Grid
}
}
impl std::convert::From<DatabaseLayout> for DatabaseLayoutPB {
fn from(rev: DatabaseLayout) -> Self {
match rev {
DatabaseLayout::Grid => DatabaseLayoutPB::Grid,
DatabaseLayout::Board => DatabaseLayoutPB::Board,
DatabaseLayout::Calendar => DatabaseLayoutPB::Calendar,
}
}
}
impl std::convert::From<DatabaseLayoutPB> for DatabaseLayout {
fn from(layout: DatabaseLayoutPB) -> Self {
match layout {
DatabaseLayoutPB::Grid => DatabaseLayout::Grid,
DatabaseLayoutPB::Board => DatabaseLayout::Board,
DatabaseLayoutPB::Calendar => DatabaseLayout::Calendar,
}
}
}
#[derive(Default, ProtoBuf)]
pub struct DatabaseSettingChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub layout_type: DatabaseLayoutPB,
#[pb(index = 3, one_of)]
pub alter_filter: Option<AlterFilterPayloadPB>,
#[pb(index = 4, one_of)]
pub delete_filter: Option<DeleteFilterPayloadPB>,
#[pb(index = 5, one_of)]
pub insert_group: Option<InsertGroupPayloadPB>,
#[pb(index = 6, one_of)]
pub delete_group: Option<DeleteGroupPayloadPB>,
#[pb(index = 7, one_of)]
pub alter_sort: Option<AlterSortPayloadPB>,
#[pb(index = 8, one_of)]
pub delete_sort: Option<DeleteSortPayloadPB>,
}
impl TryInto<DatabaseSettingChangesetParams> for DatabaseSettingChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DatabaseSettingChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
.0;
let insert_filter = match self.alter_filter {
None => None,
Some(payload) => Some(payload.try_into()?),
};
let delete_filter = match self.delete_filter {
None => None,
Some(payload) => Some(payload.try_into()?),
};
let insert_group = match self.insert_group {
Some(payload) => Some(payload.try_into()?),
None => None,
};
let delete_group = match self.delete_group {
Some(payload) => Some(payload.try_into()?),
None => None,
};
let alert_sort = match self.alter_sort {
None => None,
Some(payload) => Some(payload.try_into()?),
};
let delete_sort = match self.delete_sort {
None => None,
Some(payload) => Some(payload.try_into()?),
};
Ok(DatabaseSettingChangesetParams {
view_id,
layout_type: self.layout_type.into(),
insert_filter,
delete_filter,
insert_group,
delete_group,
alert_sort,
delete_sort,
})
}
}
pub struct DatabaseSettingChangesetParams {
pub view_id: String,
pub layout_type: DatabaseLayout,
pub insert_filter: Option<AlterFilterParams>,
pub delete_filter: Option<DeleteFilterParams>,
pub insert_group: Option<InsertGroupParams>,
pub delete_group: Option<DeleteGroupParams>,
pub alert_sort: Option<AlterSortParams>,
pub delete_sort: Option<DeleteSortParams>,
}
impl DatabaseSettingChangesetParams {
pub fn is_filter_changed(&self) -> bool {
self.insert_filter.is_some() || self.delete_filter.is_some()
}
}
#[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)]
pub struct LayoutSettingPB {
#[pb(index = 1, one_of)]
pub calendar: Option<CalendarLayoutSettingPB>,
}
#[derive(Debug, Clone, Default)]
pub struct LayoutSettingParams {
pub calendar: Option<CalendarLayoutSetting>,
}
impl From<LayoutSettingParams> for LayoutSettingPB {
fn from(data: LayoutSettingParams) -> Self {
Self {
calendar: data.calendar.map(|calendar| calendar.into()),
}
}
}
#[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)]
pub struct LayoutSettingChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2, one_of)]
pub calendar: Option<CalendarLayoutSettingPB>,
}
#[derive(Debug)]
pub struct LayoutSettingChangeset {
pub view_id: String,
pub calendar: Option<CalendarLayoutSetting>,
}
impl TryInto<LayoutSettingChangeset> for LayoutSettingChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<LayoutSettingChangeset, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
.0;
Ok(LayoutSettingChangeset {
view_id,
calendar: self.calendar.map(|calendar| calendar.into()),
})
}
}

View File

@ -0,0 +1,257 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::FieldType;
use crate::services::sort::{Sort, SortCondition, SortType};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SortPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub condition: SortConditionPB,
}
impl std::convert::From<&Sort> for SortPB {
fn from(sort: &Sort) -> Self {
Self {
id: sort.id.clone(),
field_id: sort.field_id.clone(),
field_type: sort.field_type.clone(),
condition: sort.condition.into(),
}
}
}
impl std::convert::From<Sort> for SortPB {
fn from(sort: Sort) -> Self {
Self {
id: sort.id,
field_id: sort.field_id,
field_type: sort.field_type,
condition: sort.condition.into(),
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedSortPB {
#[pb(index = 1)]
pub items: Vec<SortPB>,
}
impl std::convert::From<Vec<Sort>> for RepeatedSortPB {
fn from(revs: Vec<Sort>) -> Self {
RepeatedSortPB {
items: revs.into_iter().map(|sort| sort.into()).collect(),
}
}
}
impl std::convert::From<Vec<SortPB>> for RepeatedSortPB {
fn from(items: Vec<SortPB>) -> Self {
Self { items }
}
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum SortConditionPB {
Ascending = 0,
Descending = 1,
}
impl std::default::Default for SortConditionPB {
fn default() -> Self {
Self::Ascending
}
}
impl std::convert::From<SortCondition> for SortConditionPB {
fn from(condition: SortCondition) -> Self {
match condition {
SortCondition::Ascending => SortConditionPB::Ascending,
SortCondition::Descending => SortConditionPB::Descending,
}
}
}
impl std::convert::From<SortConditionPB> for SortCondition {
fn from(condition: SortConditionPB) -> Self {
match condition {
SortConditionPB::Ascending => SortCondition::Ascending,
SortConditionPB::Descending => SortCondition::Descending,
}
}
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct AlterSortPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
/// Create a new sort if the sort_id is None
#[pb(index = 4, one_of)]
pub sort_id: Option<String>,
#[pb(index = 5)]
pub condition: SortConditionPB,
}
impl TryInto<AlterSortParams> for AlterSortPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<AlterSortParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let sort_id = match self.sort_id {
None => None,
Some(sort_id) => Some(
NotEmptyStr::parse(sort_id)
.map_err(|_| ErrorCode::SortIdIsEmpty)?
.0,
),
};
Ok(AlterSortParams {
view_id,
field_id,
sort_id,
field_type: self.field_type,
condition: self.condition.into(),
})
}
}
#[derive(Debug)]
pub struct AlterSortParams {
pub view_id: String,
pub field_id: String,
/// Create a new sort if the sort is None
pub sort_id: Option<String>,
pub field_type: FieldType,
pub condition: SortCondition,
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct DeleteSortPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
#[pb(index = 4)]
pub sort_id: String,
}
impl TryInto<DeleteSortParams> for DeleteSortPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DeleteSortParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let sort_id = NotEmptyStr::parse(self.sort_id)
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
.0;
let sort_type = SortType {
sort_id: sort_id.clone(),
field_id,
field_type: self.field_type,
};
Ok(DeleteSortParams {
view_id,
sort_type,
sort_id,
})
}
}
#[derive(Debug, Clone)]
pub struct DeleteSortParams {
pub view_id: String,
pub sort_type: SortType,
pub sort_id: String,
}
#[derive(Debug, Default, ProtoBuf)]
pub struct SortChangesetNotificationPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub insert_sorts: Vec<SortPB>,
#[pb(index = 3)]
pub delete_sorts: Vec<SortPB>,
#[pb(index = 4)]
pub update_sorts: Vec<SortPB>,
}
impl SortChangesetNotificationPB {
pub fn new(view_id: String) -> Self {
Self {
view_id,
insert_sorts: vec![],
delete_sorts: vec![],
update_sorts: vec![],
}
}
pub fn extend(&mut self, other: SortChangesetNotificationPB) {
self.insert_sorts.extend(other.insert_sorts);
self.delete_sorts.extend(other.delete_sorts);
self.update_sorts.extend(other.update_sorts);
}
pub fn is_empty(&self) -> bool {
self.insert_sorts.is_empty() && self.delete_sorts.is_empty() && self.update_sorts.is_empty()
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct ReorderAllRowsPB {
#[pb(index = 1)]
pub row_orders: Vec<String>,
}
#[derive(Debug, Default, ProtoBuf)]
pub struct ReorderSingleRowPB {
#[pb(index = 1)]
pub row_id: i64,
#[pb(index = 2)]
pub old_index: i32,
#[pb(index = 3)]
pub new_index: i32,
}

View File

@ -0,0 +1,24 @@
use crate::services::field::CheckboxTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct CheckboxTypeOptionPB {
#[pb(index = 1)]
pub is_selected: bool,
}
impl From<CheckboxTypeOption> for CheckboxTypeOptionPB {
fn from(data: CheckboxTypeOption) -> Self {
Self {
is_selected: data.is_selected,
}
}
}
impl From<CheckboxTypeOptionPB> for CheckboxTypeOption {
fn from(data: CheckboxTypeOptionPB) -> Self {
Self {
is_selected: data.is_selected,
}
}
}

View File

@ -0,0 +1,142 @@
#![allow(clippy::upper_case_acronyms)]
use strum_macros::EnumIter;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use crate::entities::CellIdPB;
use crate::services::field::{DateFormat, DateTypeOption, TimeFormat};
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateCellDataPB {
#[pb(index = 1)]
pub date: String,
#[pb(index = 2)]
pub time: String,
#[pb(index = 3)]
pub timestamp: i64,
#[pb(index = 4)]
pub include_time: bool,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateChangesetPB {
#[pb(index = 1)]
pub cell_path: CellIdPB,
#[pb(index = 2, one_of)]
pub date: Option<String>,
#[pb(index = 3, one_of)]
pub time: Option<String>,
#[pb(index = 4, one_of)]
pub include_time: Option<bool>,
#[pb(index = 5)]
pub is_utc: bool,
}
// Date
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateTypeOptionPB {
#[pb(index = 1)]
pub date_format: DateFormatPB,
#[pb(index = 2)]
pub time_format: TimeFormatPB,
#[pb(index = 3)]
pub include_time: bool,
}
impl From<DateTypeOption> for DateTypeOptionPB {
fn from(data: DateTypeOption) -> Self {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
}
}
}
impl From<DateTypeOptionPB> for DateTypeOption {
fn from(data: DateTypeOptionPB) -> Self {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
}
}
}
#[derive(Clone, Debug, Copy, EnumIter, ProtoBuf_Enum)]
pub enum DateFormatPB {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
DayMonthYear = 4,
}
impl std::default::Default for DateFormatPB {
fn default() -> Self {
DateFormatPB::Friendly
}
}
impl From<DateFormatPB> for DateFormat {
fn from(data: DateFormatPB) -> Self {
match data {
DateFormatPB::Local => DateFormat::Local,
DateFormatPB::US => DateFormat::US,
DateFormatPB::ISO => DateFormat::ISO,
DateFormatPB::Friendly => DateFormat::Friendly,
DateFormatPB::DayMonthYear => DateFormat::DayMonthYear,
}
}
}
impl From<DateFormat> for DateFormatPB {
fn from(data: DateFormat) -> Self {
match data {
DateFormat::Local => DateFormatPB::Local,
DateFormat::US => DateFormatPB::US,
DateFormat::ISO => DateFormatPB::ISO,
DateFormat::Friendly => DateFormatPB::Friendly,
DateFormat::DayMonthYear => DateFormatPB::DayMonthYear,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, ProtoBuf_Enum)]
pub enum TimeFormatPB {
TwelveHour = 0,
TwentyFourHour = 1,
}
impl std::default::Default for TimeFormatPB {
fn default() -> Self {
TimeFormatPB::TwentyFourHour
}
}
impl From<TimeFormatPB> for TimeFormat {
fn from(data: TimeFormatPB) -> Self {
match data {
TimeFormatPB::TwelveHour => TimeFormat::TwelveHour,
TimeFormatPB::TwentyFourHour => TimeFormat::TwentyFourHour,
}
}
}
impl From<TimeFormat> for TimeFormatPB {
fn from(data: TimeFormat) -> Self {
match data {
TimeFormat::TwelveHour => TimeFormatPB::TwelveHour,
TimeFormat::TwentyFourHour => TimeFormatPB::TwentyFourHour,
}
}
}

View File

@ -0,0 +1,13 @@
mod checkbox_entities;
mod date_entities;
mod number_entities;
mod select_option;
mod text_entities;
mod url_entities;
pub use checkbox_entities::*;
pub use date_entities::*;
pub use number_entities::*;
pub use select_option::*;
pub use text_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,177 @@
use crate::services::field::{NumberFormat, NumberTypeOption};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
// Number
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct NumberTypeOptionPB {
#[pb(index = 1)]
pub format: NumberFormatPB,
#[pb(index = 2)]
pub scale: u32,
#[pb(index = 3)]
pub symbol: String,
#[pb(index = 4)]
pub sign_positive: bool,
#[pb(index = 5)]
pub name: String,
}
impl From<NumberTypeOption> for NumberTypeOptionPB {
fn from(data: NumberTypeOption) -> Self {
Self {
format: data.format.into(),
scale: data.scale,
symbol: data.symbol,
sign_positive: data.sign_positive,
name: data.name,
}
}
}
impl From<NumberTypeOptionPB> for NumberTypeOption {
fn from(data: NumberTypeOptionPB) -> Self {
Self {
format: data.format.into(),
scale: data.scale,
symbol: data.symbol,
sign_positive: data.sign_positive,
name: data.name,
}
}
}
#[derive(Clone, Copy, Debug, ProtoBuf_Enum)]
pub enum NumberFormatPB {
Num = 0,
USD = 1,
CanadianDollar = 2,
EUR = 4,
Pound = 5,
Yen = 6,
Ruble = 7,
Rupee = 8,
Won = 9,
Yuan = 10,
Real = 11,
Lira = 12,
Rupiah = 13,
Franc = 14,
HongKongDollar = 15,
NewZealandDollar = 16,
Krona = 17,
NorwegianKrone = 18,
MexicanPeso = 19,
Rand = 20,
NewTaiwanDollar = 21,
DanishKrone = 22,
Baht = 23,
Forint = 24,
Koruna = 25,
Shekel = 26,
ChileanPeso = 27,
PhilippinePeso = 28,
Dirham = 29,
ColombianPeso = 30,
Riyal = 31,
Ringgit = 32,
Leu = 33,
ArgentinePeso = 34,
UruguayanPeso = 35,
Percent = 36,
}
impl std::default::Default for NumberFormatPB {
fn default() -> Self {
NumberFormatPB::Num
}
}
impl From<NumberFormat> for NumberFormatPB {
fn from(data: NumberFormat) -> Self {
match data {
NumberFormat::Num => NumberFormatPB::Num,
NumberFormat::USD => NumberFormatPB::USD,
NumberFormat::CanadianDollar => NumberFormatPB::CanadianDollar,
NumberFormat::EUR => NumberFormatPB::EUR,
NumberFormat::Pound => NumberFormatPB::Pound,
NumberFormat::Yen => NumberFormatPB::Yen,
NumberFormat::Ruble => NumberFormatPB::Ruble,
NumberFormat::Rupee => NumberFormatPB::Rupee,
NumberFormat::Won => NumberFormatPB::Won,
NumberFormat::Yuan => NumberFormatPB::Yuan,
NumberFormat::Real => NumberFormatPB::Real,
NumberFormat::Lira => NumberFormatPB::Lira,
NumberFormat::Rupiah => NumberFormatPB::Rupiah,
NumberFormat::Franc => NumberFormatPB::Franc,
NumberFormat::HongKongDollar => NumberFormatPB::HongKongDollar,
NumberFormat::NewZealandDollar => NumberFormatPB::NewZealandDollar,
NumberFormat::Krona => NumberFormatPB::Krona,
NumberFormat::NorwegianKrone => NumberFormatPB::NorwegianKrone,
NumberFormat::MexicanPeso => NumberFormatPB::MexicanPeso,
NumberFormat::Rand => NumberFormatPB::Rand,
NumberFormat::NewTaiwanDollar => NumberFormatPB::NewTaiwanDollar,
NumberFormat::DanishKrone => NumberFormatPB::DanishKrone,
NumberFormat::Baht => NumberFormatPB::Baht,
NumberFormat::Forint => NumberFormatPB::Forint,
NumberFormat::Koruna => NumberFormatPB::Koruna,
NumberFormat::Shekel => NumberFormatPB::Shekel,
NumberFormat::ChileanPeso => NumberFormatPB::ChileanPeso,
NumberFormat::PhilippinePeso => NumberFormatPB::PhilippinePeso,
NumberFormat::Dirham => NumberFormatPB::Dirham,
NumberFormat::ColombianPeso => NumberFormatPB::ColombianPeso,
NumberFormat::Riyal => NumberFormatPB::Riyal,
NumberFormat::Ringgit => NumberFormatPB::Ringgit,
NumberFormat::Leu => NumberFormatPB::Leu,
NumberFormat::ArgentinePeso => NumberFormatPB::ArgentinePeso,
NumberFormat::UruguayanPeso => NumberFormatPB::UruguayanPeso,
NumberFormat::Percent => NumberFormatPB::Percent,
}
}
}
impl From<NumberFormatPB> for NumberFormat {
fn from(data: NumberFormatPB) -> Self {
match data {
NumberFormatPB::Num => NumberFormat::Num,
NumberFormatPB::USD => NumberFormat::USD,
NumberFormatPB::CanadianDollar => NumberFormat::CanadianDollar,
NumberFormatPB::EUR => NumberFormat::EUR,
NumberFormatPB::Pound => NumberFormat::Pound,
NumberFormatPB::Yen => NumberFormat::Yen,
NumberFormatPB::Ruble => NumberFormat::Ruble,
NumberFormatPB::Rupee => NumberFormat::Rupee,
NumberFormatPB::Won => NumberFormat::Won,
NumberFormatPB::Yuan => NumberFormat::Yuan,
NumberFormatPB::Real => NumberFormat::Real,
NumberFormatPB::Lira => NumberFormat::Lira,
NumberFormatPB::Rupiah => NumberFormat::Rupiah,
NumberFormatPB::Franc => NumberFormat::Franc,
NumberFormatPB::HongKongDollar => NumberFormat::HongKongDollar,
NumberFormatPB::NewZealandDollar => NumberFormat::NewZealandDollar,
NumberFormatPB::Krona => NumberFormat::Krona,
NumberFormatPB::NorwegianKrone => NumberFormat::NorwegianKrone,
NumberFormatPB::MexicanPeso => NumberFormat::MexicanPeso,
NumberFormatPB::Rand => NumberFormat::Rand,
NumberFormatPB::NewTaiwanDollar => NumberFormat::NewTaiwanDollar,
NumberFormatPB::DanishKrone => NumberFormat::DanishKrone,
NumberFormatPB::Baht => NumberFormat::Baht,
NumberFormatPB::Forint => NumberFormat::Forint,
NumberFormatPB::Koruna => NumberFormat::Koruna,
NumberFormatPB::Shekel => NumberFormat::Shekel,
NumberFormatPB::ChileanPeso => NumberFormat::ChileanPeso,
NumberFormatPB::PhilippinePeso => NumberFormat::PhilippinePeso,
NumberFormatPB::Dirham => NumberFormat::Dirham,
NumberFormatPB::ColombianPeso => NumberFormat::ColombianPeso,
NumberFormatPB::Riyal => NumberFormat::Riyal,
NumberFormatPB::Ringgit => NumberFormat::Ringgit,
NumberFormatPB::Leu => NumberFormat::Leu,
NumberFormatPB::ArgentinePeso => NumberFormat::ArgentinePeso,
NumberFormatPB::UruguayanPeso => NumberFormat::UruguayanPeso,
NumberFormatPB::Percent => NumberFormat::Percent,
}
}
}

View File

@ -0,0 +1,320 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::{CellIdPB, CellIdParams};
use crate::services::field::{
ChecklistTypeOption, MultiSelectTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption,
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
/// [SelectOptionPB] represents an option for a single select, and multiple select.
#[derive(Clone, Debug, Default, PartialEq, Eq, ProtoBuf)]
pub struct SelectOptionPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub name: String,
#[pb(index = 3)]
pub color: SelectOptionColorPB,
}
impl From<SelectOption> for SelectOptionPB {
fn from(data: SelectOption) -> Self {
Self {
id: data.id,
name: data.name,
color: data.color.into(),
}
}
}
impl From<SelectOptionPB> for SelectOption {
fn from(data: SelectOptionPB) -> Self {
Self {
id: data.id,
name: data.name,
color: data.color.into(),
}
}
}
#[derive(Default, ProtoBuf)]
pub struct RepeatedSelectOptionPayload {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub row_id: i64,
#[pb(index = 4)]
pub items: Vec<SelectOptionPB>,
}
#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone)]
#[repr(u8)]
pub enum SelectOptionColorPB {
Purple = 0,
Pink = 1,
LightPink = 2,
Orange = 3,
Yellow = 4,
Lime = 5,
Green = 6,
Aqua = 7,
Blue = 8,
}
impl std::default::Default for SelectOptionColorPB {
fn default() -> Self {
SelectOptionColorPB::Purple
}
}
impl From<SelectOptionColor> for SelectOptionColorPB {
fn from(data: SelectOptionColor) -> Self {
match data {
SelectOptionColor::Purple => SelectOptionColorPB::Purple,
SelectOptionColor::Pink => SelectOptionColorPB::Pink,
SelectOptionColor::LightPink => SelectOptionColorPB::LightPink,
SelectOptionColor::Orange => SelectOptionColorPB::Orange,
SelectOptionColor::Yellow => SelectOptionColorPB::Yellow,
SelectOptionColor::Lime => SelectOptionColorPB::Lime,
SelectOptionColor::Green => SelectOptionColorPB::Green,
SelectOptionColor::Aqua => SelectOptionColorPB::Aqua,
SelectOptionColor::Blue => SelectOptionColorPB::Blue,
}
}
}
impl From<SelectOptionColorPB> for SelectOptionColor {
fn from(data: SelectOptionColorPB) -> Self {
match data {
SelectOptionColorPB::Purple => SelectOptionColor::Purple,
SelectOptionColorPB::Pink => SelectOptionColor::Pink,
SelectOptionColorPB::LightPink => SelectOptionColor::LightPink,
SelectOptionColorPB::Orange => SelectOptionColor::Orange,
SelectOptionColorPB::Yellow => SelectOptionColor::Yellow,
SelectOptionColorPB::Lime => SelectOptionColor::Lime,
SelectOptionColorPB::Green => SelectOptionColor::Green,
SelectOptionColorPB::Aqua => SelectOptionColor::Aqua,
SelectOptionColorPB::Blue => SelectOptionColor::Blue,
}
}
}
/// [SelectOptionCellDataPB] contains a list of user's selected options and a list of all the options
/// that the cell can use.
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SelectOptionCellDataPB {
/// The available options that the cell can use.
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
/// The selected options for the cell.
#[pb(index = 2)]
pub select_options: Vec<SelectOptionPB>,
}
/// [SelectOptionChangesetPB] describes the changes of a FieldTypeOptionData. For the moment,
/// it is used by [MultiSelectTypeOptionPB] and [SingleSelectTypeOptionPB].
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SelectOptionChangesetPB {
#[pb(index = 1)]
pub cell_identifier: CellIdPB,
#[pb(index = 2)]
pub insert_options: Vec<SelectOptionPB>,
#[pb(index = 3)]
pub update_options: Vec<SelectOptionPB>,
#[pb(index = 4)]
pub delete_options: Vec<SelectOptionPB>,
}
pub struct SelectOptionChangeset {
pub cell_path: CellIdParams,
pub insert_options: Vec<SelectOptionPB>,
pub update_options: Vec<SelectOptionPB>,
pub delete_options: Vec<SelectOptionPB>,
}
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionChangeset, Self::Error> {
let cell_identifier = self.cell_identifier.try_into()?;
Ok(SelectOptionChangeset {
cell_path: cell_identifier,
insert_options: self.insert_options,
update_options: self.update_options,
delete_options: self.delete_options,
})
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SelectOptionCellChangesetPB {
#[pb(index = 1)]
pub cell_identifier: CellIdPB,
#[pb(index = 2)]
pub insert_option_ids: Vec<String>,
#[pb(index = 3)]
pub delete_option_ids: Vec<String>,
}
pub struct SelectOptionCellChangesetParams {
pub cell_identifier: CellIdParams,
pub insert_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
}
impl TryInto<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPB {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
let cell_identifier: CellIdParams = self.cell_identifier.try_into()?;
let insert_option_ids = self
.insert_option_ids
.into_iter()
.flat_map(|option_id| match NotEmptyStr::parse(option_id) {
Ok(option_id) => Some(option_id.0),
Err(_) => {
tracing::error!("The insert option id should not be empty");
None
},
})
.collect::<Vec<String>>();
let delete_option_ids = self
.delete_option_ids
.into_iter()
.flat_map(|option_id| match NotEmptyStr::parse(option_id) {
Ok(option_id) => Some(option_id.0),
Err(_) => {
tracing::error!("The deleted option id should not be empty");
None
},
})
.collect::<Vec<String>>();
Ok(SelectOptionCellChangesetParams {
cell_identifier,
insert_option_ids,
delete_option_ids,
})
}
}
// Single select
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SingleSelectTypeOptionPB {
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
#[pb(index = 2)]
pub disable_color: bool,
}
impl From<SingleSelectTypeOption> for SingleSelectTypeOptionPB {
fn from(data: SingleSelectTypeOption) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}
impl From<SingleSelectTypeOptionPB> for SingleSelectTypeOption {
fn from(data: SingleSelectTypeOptionPB) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct MultiSelectTypeOptionPB {
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
#[pb(index = 2)]
pub disable_color: bool,
}
impl From<MultiSelectTypeOption> for MultiSelectTypeOptionPB {
fn from(data: MultiSelectTypeOption) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}
impl From<MultiSelectTypeOptionPB> for MultiSelectTypeOption {
fn from(data: MultiSelectTypeOptionPB) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct ChecklistTypeOptionPB {
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
#[pb(index = 2)]
pub disable_color: bool,
}
impl From<ChecklistTypeOption> for ChecklistTypeOptionPB {
fn from(data: ChecklistTypeOption) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}
impl From<ChecklistTypeOptionPB> for ChecklistTypeOption {
fn from(data: ChecklistTypeOptionPB) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
}
}

View File

@ -0,0 +1,20 @@
use crate::services::field::RichTextTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RichTextTypeOptionPB {
#[pb(index = 1)]
data: String,
}
impl From<RichTextTypeOption> for RichTextTypeOptionPB {
fn from(data: RichTextTypeOption) -> Self {
Self { data: data.inner }
}
}
impl From<RichTextTypeOptionPB> for RichTextTypeOption {
fn from(data: RichTextTypeOptionPB) -> Self {
Self { inner: data.data }
}
}

View File

@ -0,0 +1,38 @@
use crate::services::field::URLTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct URLCellDataPB {
#[pb(index = 1)]
pub url: String,
#[pb(index = 2)]
pub content: String,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct URLTypeOptionPB {
#[pb(index = 1)]
pub url: String,
#[pb(index = 2)]
pub content: String,
}
impl From<URLTypeOption> for URLTypeOptionPB {
fn from(data: URLTypeOption) -> Self {
Self {
url: data.url,
content: data.content,
}
}
}
impl From<URLTypeOptionPB> for URLTypeOption {
fn from(data: URLTypeOptionPB) -> Self {
Self {
url: data.url,
content: data.content,
}
}
}

View File

@ -0,0 +1,69 @@
use flowy_derive::ProtoBuf;
use crate::entities::{InsertedRowPB, UpdatedRowPB};
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct RowsVisibilityChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 5)]
pub visible_rows: Vec<InsertedRowPB>,
#[pb(index = 6)]
pub invisible_rows: Vec<i64>,
}
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct RowsChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub inserted_rows: Vec<InsertedRowPB>,
#[pb(index = 3)]
pub deleted_rows: Vec<i64>,
#[pb(index = 4)]
pub updated_rows: Vec<UpdatedRowPB>,
}
impl RowsChangesetPB {
pub fn from_insert(view_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {
Self {
view_id,
inserted_rows,
..Default::default()
}
}
pub fn from_delete(view_id: String, deleted_rows: Vec<i64>) -> Self {
Self {
view_id,
deleted_rows,
..Default::default()
}
}
pub fn from_update(view_id: String, updated_rows: Vec<UpdatedRowPB>) -> Self {
Self {
view_id,
updated_rows,
..Default::default()
}
}
pub fn from_move(
view_id: String,
deleted_rows: Vec<i64>,
inserted_rows: Vec<InsertedRowPB>,
) -> Self {
Self {
view_id,
inserted_rows,
deleted_rows,
..Default::default()
}
}
}

View File

@ -0,0 +1,605 @@
use std::sync::Arc;
use collab_database::rows::RowId;
use collab_database::views::DatabaseLayout;
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use crate::entities::*;
use crate::manager::DatabaseManager2;
use crate::services::field::{
type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset,
};
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_database_data_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<DatabasePB, FlowyError> {
let view_id: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(view_id.as_ref()).await?;
let data = database_editor.get_database_data(view_id.as_ref()).await;
data_result_ok(data)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_database_setting_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<DatabaseViewSettingPB, FlowyError> {
let view_id: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(view_id.as_ref()).await?;
let data = database_editor
.get_database_view_setting(view_id.as_ref())
.await?;
data_result_ok(data)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn update_database_setting_handler(
data: AFPluginData<DatabaseSettingChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: DatabaseSettingChangesetParams = data.into_inner().try_into()?;
let editor = manager.get_database(&params.view_id).await?;
if let Some(insert_params) = params.insert_group {
editor.insert_group(insert_params).await?;
}
if let Some(delete_params) = params.delete_group {
editor.delete_group(delete_params).await?;
}
if let Some(alter_filter) = params.insert_filter {
editor.create_or_update_filter(alter_filter).await?;
}
if let Some(delete_filter) = params.delete_filter {
editor.delete_filter(delete_filter).await?;
}
if let Some(alter_sort) = params.alert_sort {
let _ = editor.create_or_update_sort(alter_sort).await?;
}
if let Some(delete_sort) = params.delete_sort {
editor.delete_sort(delete_sort).await?;
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_all_filters_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedFilterPB, FlowyError> {
let view_id: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(view_id.as_ref()).await?;
let filters = database_editor.get_all_filters(view_id.as_ref()).await;
data_result_ok(filters)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_all_sorts_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedSortPB, FlowyError> {
let view_id: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(view_id.as_ref()).await?;
let sorts = database_editor.get_all_sorts(view_id.as_ref()).await;
data_result_ok(sorts)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn delete_all_sorts_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let view_id: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(view_id.as_ref()).await?;
database_editor.delete_all_sorts(view_id.as_ref()).await;
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_fields_handler(
data: AFPluginData<GetFieldPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedFieldPB, FlowyError> {
let params: GetFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let fields = database_editor
.get_fields(&params.view_id, params.field_ids)
.into_iter()
.map(FieldPB::from)
.collect::<Vec<FieldPB>>()
.into();
data_result_ok(fields)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn update_field_handler(
data: AFPluginData<FieldChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: FieldChangesetParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor.update_field(params).await?;
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn update_field_type_option_handler(
data: AFPluginData<TypeOptionChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: TypeOptionChangesetParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
if let Some(old_field) = database_editor.get_field(&params.field_id) {
let field_type = FieldType::from(old_field.field_type);
let type_option_data =
type_option_data_from_pb_or_default(params.type_option_data, &field_type);
database_editor
.update_field_type_option(
&params.view_id,
&params.field_id,
type_option_data,
old_field,
)
.await?;
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn delete_field_handler(
data: AFPluginData<DeleteFieldPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: FieldIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor.delete_field(&params.field_id).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn switch_to_field_handler(
data: AFPluginData<UpdateFieldTypePayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: EditFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let old_field = database_editor.get_field(&params.field_id);
database_editor
.switch_to_field_type(&params.field_id, &params.field_type)
.await?;
if let Some(new_type_option) = database_editor
.get_field(&params.field_id)
.map(|field| field.get_any_type_option(field.field_type))
{
match (old_field, new_type_option) {
(Some(old_field), Some(new_type_option)) => {
database_editor
.update_field_type_option(
&params.view_id,
&params.field_id,
new_type_option,
old_field,
)
.await?;
},
_ => {
tracing::warn!("Old field and the new type option should not be empty");
},
}
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn duplicate_field_handler(
data: AFPluginData<DuplicateFieldPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: FieldIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.duplicate_field(&params.view_id, &params.field_id)
.await?;
Ok(())
}
/// Return the FieldTypeOptionData if the Field exists otherwise return record not found error.
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_field_type_option_data_handler(
data: AFPluginData<TypeOptionPathPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<TypeOptionPB, FlowyError> {
let params: TypeOptionPathParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
if let Some((field, data)) = database_editor
.get_field_type_option_data(&params.field_id)
.await
{
let data = TypeOptionPB {
view_id: params.view_id,
field: FieldPB::from(field),
type_option_data: data.to_vec(),
};
data_result_ok(data)
} else {
Err(FlowyError::record_not_found())
}
}
/// Create FieldMeta and save it. Return the FieldTypeOptionData.
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn create_field_type_option_data_handler(
data: AFPluginData<CreateFieldPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<TypeOptionPB, FlowyError> {
let params: CreateFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let (field, data) = database_editor
.create_field_with_type_option(&params.view_id, &params.field_type, params.type_option_data)
.await;
let data = TypeOptionPB {
view_id: params.view_id,
field: FieldPB::from(field),
type_option_data: data.to_vec(),
};
data_result_ok(data)
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn move_field_handler(
data: AFPluginData<MoveFieldPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: MoveFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.move_field(
&params.view_id,
&params.field_id,
params.from_index,
params.to_index,
)
.await?;
Ok(())
}
// #[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_row_handler(
data: AFPluginData<RowIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<OptionalRowPB, FlowyError> {
let params: RowIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let row = database_editor.get_row(params.row_id).map(RowPB::from);
data_result_ok(OptionalRowPB { row })
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn delete_row_handler(
data: AFPluginData<RowIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: RowIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor.delete_row(params.row_id).await;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn duplicate_row_handler(
data: AFPluginData<RowIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: RowIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.duplicate_row(&params.view_id, params.row_id)
.await;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn move_row_handler(
data: AFPluginData<MoveRowPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: MoveRowParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.move_row(&params.view_id, params.from_row_id, params.to_row_id)
.await;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn create_row_handler(
data: AFPluginData<CreateRowPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RowPB, FlowyError> {
let params: CreateRowParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
match database_editor.create_row(params).await? {
None => Err(FlowyError::internal().context("Create row fail")),
Some(row) => data_result_ok(RowPB::from(row)),
}
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_cell_handler(
data: AFPluginData<CellIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<CellPB, FlowyError> {
let params: CellIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let cell = database_editor
.get_cell(&params.field_id, params.row_id)
.await;
data_result_ok(cell)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_cell_handler(
data: AFPluginData<CellChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: CellChangesetPB = data.into_inner();
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.update_cell_with_changeset(
&params.view_id,
RowId::from(params.row_id),
&params.field_id,
params.cell_changeset.clone(),
)
.await;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn new_select_option_handler(
data: AFPluginData<CreateSelectOptionPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<SelectOptionPB, FlowyError> {
let params: CreateSelectOptionParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let result = database_editor
.create_select_option(&params.field_id, params.option_name)
.await;
match result {
None => {
Err(FlowyError::record_not_found().context("Create select option fail. Can't find the field"))
},
Some(pb) => data_result_ok(pb),
}
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn insert_or_update_select_option_handler(
data: AFPluginData<RepeatedSelectOptionPayload>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params = data.into_inner();
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.insert_select_options(
&params.view_id,
&params.field_id,
RowId::from(params.row_id),
params.items,
)
.await;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn delete_select_option_handler(
data: AFPluginData<RepeatedSelectOptionPayload>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params = data.into_inner();
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.delete_select_options(
&params.view_id,
&params.field_id,
RowId::from(params.row_id),
params.items,
)
.await;
Ok(())
}
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_select_option_handler(
data: AFPluginData<CellIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<SelectOptionCellDataPB, FlowyError> {
let params: CellIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let options = database_editor
.get_select_options(params.row_id, &params.field_id)
.await;
data_result_ok(options)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_select_option_cell_handler(
data: AFPluginData<SelectOptionCellChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?;
let database_editor = manager
.get_database(&params.cell_identifier.view_id)
.await?;
let changeset = SelectOptionCellChangeset {
insert_option_ids: params.insert_option_ids,
delete_option_ids: params.delete_option_ids,
};
database_editor
.update_cell_with_changeset(
&params.cell_identifier.view_id,
params.cell_identifier.row_id,
&params.cell_identifier.field_id,
changeset,
)
.await;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_date_cell_handler(
data: AFPluginData<DateChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let data = data.into_inner();
let cell_id: CellIdParams = data.cell_path.try_into()?;
let cell_changeset = DateCellChangeset {
date: data.date,
time: data.time,
include_time: data.include_time,
is_utc: data.is_utc,
};
let database_editor = manager.get_database(&cell_id.view_id).await?;
database_editor
.update_cell_with_changeset(
&cell_id.view_id,
cell_id.row_id,
&cell_id.field_id,
cell_changeset,
)
.await;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_groups_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedGroupPB, FlowyError> {
let params: DatabaseViewIdPB = data.into_inner();
let database_editor = manager.get_database(params.as_ref()).await?;
let groups = database_editor.load_groups(params.as_ref()).await?;
data_result_ok(groups)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_group_handler(
data: AFPluginData<DatabaseGroupIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<GroupPB, FlowyError> {
let params: DatabaseGroupIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let group = database_editor
.get_group(&params.view_id, &params.group_id)
.await?;
data_result_ok(group)
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn move_group_handler(
data: AFPluginData<MoveGroupPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> FlowyResult<()> {
let params: MoveGroupParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.move_group(&params.view_id, &params.from_group_id, &params.to_group_id)
.await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn move_group_row_handler(
data: AFPluginData<MoveGroupRowPayloadPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> FlowyResult<()> {
let params: MoveGroupRowParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
database_editor
.move_group_row(
&params.view_id,
&params.to_group_id,
params.from_row_id,
params.to_row_id,
)
.await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(manager), err)]
pub(crate) async fn get_databases_handler(
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedDatabaseDescriptionPB, FlowyError> {
let data = manager.get_all_databases_description().await;
data_result_ok(data)
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn set_layout_setting_handler(
data: AFPluginData<LayoutSettingChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> FlowyResult<()> {
let params: LayoutSettingChangeset = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let layout_params = LayoutSettingParams {
calendar: params.calendar,
};
database_editor
.set_layout_setting(&params.view_id, DatabaseLayout::Calendar, layout_params)
.await;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_layout_setting_handler(
data: AFPluginData<DatabaseLayoutIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<LayoutSettingPB, FlowyError> {
let params: DatabaseLayoutId = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let layout_setting_pb = database_editor
.get_layout_setting(&params.view_id, params.layout)
.await
.map(LayoutSettingPB::from)
.unwrap_or_default();
data_result_ok(layout_setting_pb)
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_calendar_events_handler(
data: AFPluginData<CalendarEventRequestPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<RepeatedCalendarEventPB, FlowyError> {
let params: CalendarEventRequestParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let events = database_editor
.get_all_calendar_events(&params.view_id)
.await;
data_result_ok(RepeatedCalendarEventPB { items: events })
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_calendar_event_handler(
data: AFPluginData<RowIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<CalendarEventPB, FlowyError> {
let params: RowIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database(&params.view_id).await?;
let event = database_editor
.get_calendar_event(&params.view_id, params.row_id)
.await;
match event {
None => Err(FlowyError::record_not_found()),
Some(event) => data_result_ok(event),
}
}

View File

@ -0,0 +1,265 @@
use std::sync::Arc;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use lib_dispatch::prelude::*;
use crate::event_handler::*;
use crate::manager::DatabaseManager2;
pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
let mut plugin = AFPlugin::new()
.name(env!("CARGO_PKG_NAME"))
.state(database_manager);
plugin = plugin
.event(DatabaseEvent::GetDatabase, get_database_data_handler)
.event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler)
.event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler)
.event(DatabaseEvent::GetAllFilters, get_all_filters_handler)
.event(DatabaseEvent::GetAllSorts, get_all_sorts_handler)
.event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler)
// Field
.event(DatabaseEvent::GetFields, get_fields_handler)
.event(DatabaseEvent::UpdateField, update_field_handler)
.event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler)
.event(DatabaseEvent::DeleteField, delete_field_handler)
.event(DatabaseEvent::UpdateFieldType, switch_to_field_handler)
.event(DatabaseEvent::DuplicateField, duplicate_field_handler)
.event(DatabaseEvent::MoveField, move_field_handler)
.event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler)
.event(DatabaseEvent::CreateTypeOption, create_field_type_option_data_handler)
// Row
.event(DatabaseEvent::CreateRow, create_row_handler)
.event(DatabaseEvent::GetRow, get_row_handler)
.event(DatabaseEvent::DeleteRow, delete_row_handler)
.event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
.event(DatabaseEvent::MoveRow, move_row_handler)
// Cell
.event(DatabaseEvent::GetCell, get_cell_handler)
.event(DatabaseEvent::UpdateCell, update_cell_handler)
// SelectOption
.event(DatabaseEvent::CreateSelectOption, new_select_option_handler)
.event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler)
.event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler)
.event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler)
.event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler)
// Date
.event(DatabaseEvent::UpdateDateCell, update_date_cell_handler)
// Group
.event(DatabaseEvent::MoveGroup, move_group_handler)
.event(DatabaseEvent::MoveGroupRow, move_group_row_handler)
.event(DatabaseEvent::GetGroups, get_groups_handler)
.event(DatabaseEvent::GetGroup, get_group_handler)
// Database
.event(DatabaseEvent::GetDatabases, get_databases_handler)
// Calendar
.event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler)
.event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
// Layout setting
.event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler);
plugin
}
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
/// out, it includes how to use these annotations: input, output, etc.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum DatabaseEvent {
/// [GetDatabase] event is used to get the [DatabasePB]
///
/// The event handler accepts a [DatabaseViewIdPB] and returns a [DatabasePB] if there are no errors.
#[event(input = "DatabaseViewIdPB", output = "DatabasePB")]
GetDatabase = 0,
/// [GetDatabaseSetting] event is used to get the database's settings.
///
/// The event handler accepts [DatabaseViewIdPB] and return [DatabaseViewSettingPB]
/// if there is no errors.
#[event(input = "DatabaseViewIdPB", output = "DatabaseViewSettingPB")]
GetDatabaseSetting = 2,
/// [UpdateDatabaseSetting] event is used to update the database's settings.
///
/// The event handler accepts [DatabaseSettingChangesetPB] and return errors if failed to modify the grid's settings.
#[event(input = "DatabaseSettingChangesetPB")]
UpdateDatabaseSetting = 3,
#[event(input = "DatabaseViewIdPB", output = "RepeatedFilterPB")]
GetAllFilters = 4,
#[event(input = "DatabaseViewIdPB", output = "RepeatedSortPB")]
GetAllSorts = 5,
#[event(input = "DatabaseViewIdPB")]
DeleteAllSorts = 6,
/// [GetFields] event is used to get the database's fields.
///
/// The event handler accepts a [GetFieldPayloadPB] and returns a [RepeatedFieldPB]
/// if there are no errors.
#[event(input = "GetFieldPayloadPB", output = "RepeatedFieldPB")]
GetFields = 10,
/// [UpdateField] event is used to update a field's attributes.
///
/// The event handler accepts a [FieldChangesetPB] and returns errors if failed to modify the
/// field.
#[event(input = "FieldChangesetPB")]
UpdateField = 11,
/// [UpdateFieldTypeOption] event is used to update the field's type-option data. Certain field
/// types have user-defined options such as color, date format, number format, or a list of values
/// for a multi-select list. These options are defined within a specialization of the
/// FieldTypeOption class.
///
/// Check out [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype)
/// for more information.
///
/// The event handler accepts a [TypeOptionChangesetPB] and returns errors if failed to modify the
/// field.
#[event(input = "TypeOptionChangesetPB")]
UpdateFieldTypeOption = 12,
/// [DeleteField] event is used to delete a Field. [DeleteFieldPayloadPB] is the context that
/// is used to delete the field from the Database.
#[event(input = "DeleteFieldPayloadPB")]
DeleteField = 14,
/// [UpdateFieldType] event is used to update the current Field's type.
/// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise
/// reuse the existing FieldTypeOptionData. You could check the [DatabaseRevisionPad] for more details.
#[event(input = "UpdateFieldTypePayloadPB")]
UpdateFieldType = 20,
/// [DuplicateField] event is used to duplicate a Field. The duplicated field data is kind of
/// deep copy of the target field. The passed in [DuplicateFieldPayloadPB] is the context that is
/// used to duplicate the field.
///
/// Return errors if failed to duplicate the field.
///
#[event(input = "DuplicateFieldPayloadPB")]
DuplicateField = 21,
/// [MoveItem] event is used to move an item. For the moment, Item has two types defined in
/// [MoveItemTypePB].
#[event(input = "MoveFieldPayloadPB")]
MoveField = 22,
/// [TypeOptionPathPB] event is used to get the FieldTypeOption data for a specific field type.
///
/// Check out the [TypeOptionPB] for more details. If the [FieldTypeOptionData] does exist
/// for the target type, the [TypeOptionBuilder] will create the default data for that type.
///
/// Return the [TypeOptionPB] if there are no errors.
#[event(input = "TypeOptionPathPB", output = "TypeOptionPB")]
GetTypeOption = 23,
/// [CreateTypeOption] event is used to create a new FieldTypeOptionData.
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
CreateTypeOption = 24,
/// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if
/// there are no errors.
#[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")]
CreateSelectOption = 30,
/// [GetSelectOptionCellData] event is used to get the select option data for cell editing.
/// [CellIdPB] locate which cell data that will be read from. The return value, [SelectOptionCellDataPB]
/// contains the available options and the currently selected options.
#[event(input = "CellIdPB", output = "SelectOptionCellDataPB")]
GetSelectOptionCellData = 31,
/// [InsertOrUpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is
/// FieldType::SingleSelect or FieldType::MultiSelect.
///
/// This event may trigger the DatabaseNotification::DidUpdateCell event.
/// For example, DatabaseNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
/// carries a change that updates the name of the option.
#[event(input = "RepeatedSelectOptionPayload")]
InsertOrUpdateSelectOption = 32,
#[event(input = "RepeatedSelectOptionPayload")]
DeleteSelectOption = 33,
#[event(input = "CreateRowPayloadPB", output = "RowPB")]
CreateRow = 50,
/// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables
/// to return a nullable row data.
#[event(input = "RowIdPB", output = "OptionalRowPB")]
GetRow = 51,
#[event(input = "RowIdPB")]
DeleteRow = 52,
#[event(input = "RowIdPB")]
DuplicateRow = 53,
#[event(input = "MoveRowPayloadPB")]
MoveRow = 54,
#[event(input = "CellIdPB", output = "CellPB")]
GetCell = 70,
/// [UpdateCell] event is used to update the cell content. The passed in data, [CellChangesetPB],
/// carries the changes that will be applied to the cell content by calling `update_cell` function.
///
/// The 'content' property of the [CellChangesetPB] is a String type. It can be used directly if the
/// cell uses string data. For example, the TextCell or NumberCell.
///
/// But,it can be treated as a generic type, because we can use [serde] to deserialize the string
/// into a specific data type. For the moment, the 'content' will be deserialized to a concrete type
/// when the FieldType is SingleSelect, DateTime, and MultiSelect. Please see
/// the [UpdateSelectOptionCell] and [UpdateDateCell] events for more details.
#[event(input = "CellChangesetPB")]
UpdateCell = 71,
/// [UpdateSelectOptionCell] event is used to update a select option cell's data. [SelectOptionCellChangesetPB]
/// contains options that will be deleted or inserted. It can be cast to [CellChangesetPB] that
/// will be used by the `update_cell` function.
#[event(input = "SelectOptionCellChangesetPB")]
UpdateSelectOptionCell = 72,
/// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB]
/// contains the date and the time string. It can be cast to [CellChangesetPB] that
/// will be used by the `update_cell` function.
#[event(input = "DateChangesetPB")]
UpdateDateCell = 80,
#[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")]
GetGroups = 100,
#[event(input = "DatabaseGroupIdPB", output = "GroupPB")]
GetGroup = 101,
#[event(input = "MoveGroupPayloadPB")]
MoveGroup = 111,
#[event(input = "MoveGroupRowPayloadPB")]
MoveGroupRow = 112,
#[event(input = "MoveGroupRowPayloadPB")]
GroupByField = 113,
/// Returns all the databases
#[event(output = "RepeatedDatabaseDescriptionPB")]
GetDatabases = 114,
#[event(input = "LayoutSettingChangesetPB")]
SetLayoutSetting = 115,
#[event(input = "DatabaseLayoutIdPB", output = "LayoutSettingPB")]
GetLayoutSetting = 116,
#[event(input = "CalendarEventRequestPB", output = "RepeatedCalendarEventPB")]
GetAllCalendarEvents = 117,
#[event(input = "RowIdPB", output = "CalendarEventPB")]
GetCalendarEvent = 118,
#[event(input = "MoveCalendarEventPB")]
MoveCalendarEvent = 119,
}

View File

@ -0,0 +1,10 @@
pub use manager::*;
pub mod entities;
mod event_handler;
pub mod event_map;
mod manager;
mod notification;
mod protobuf;
pub mod services;
pub mod template;

View File

@ -0,0 +1,215 @@
use collab::plugin_impl::rocks_disk::Config;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
use collab_database::database::DatabaseData;
use collab_database::user::UserDatabase as InnerUserDatabase;
use collab_database::views::{CreateDatabaseParams, CreateViewParams};
use collab_persistence::kv::rocks_kv::RocksCollabDB;
use parking_lot::Mutex;
use tokio::sync::RwLock;
use flowy_error::{FlowyError, FlowyResult};
use flowy_task::TaskDispatcher;
use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB};
use crate::services::database::{DatabaseEditor, MutexDatabase};
pub trait DatabaseUser2: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<String, FlowyError>;
fn kv_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}
pub struct DatabaseManager2 {
user: Arc<dyn DatabaseUser2>,
user_database: UserDatabase,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
editors: RwLock<HashMap<String, Arc<DatabaseEditor>>>,
}
impl DatabaseManager2 {
pub fn new(
database_user: Arc<dyn DatabaseUser2>,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
) -> Self {
Self {
user: database_user,
user_database: UserDatabase::default(),
task_scheduler,
editors: Default::default(),
}
}
pub async fn initialize(&self, user_id: i64, _token: &str) -> FlowyResult<()> {
let db = self.user.kv_db()?;
*self.user_database.lock() = Some(InnerUserDatabase::new(
user_id,
db,
Config::default()
.enable_snapshot(true)
.snapshot_per_update(10),
));
// do nothing
Ok(())
}
pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> {
self.initialize(user_id, token).await?;
Ok(())
}
pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB {
let databases_description = self.with_user_database(vec![], |database| {
database
.get_all_databases()
.into_iter()
.map(DatabaseDescriptionPB::from)
.collect()
});
RepeatedDatabaseDescriptionPB {
items: databases_description,
}
}
pub async fn get_database(&self, view_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
let database_id = self.with_user_database(Err(FlowyError::internal()), |database| {
database
.get_database_id_with_view_id(view_id)
.ok_or_else(FlowyError::record_not_found)
})?;
if let Some(editor) = self.editors.read().await.get(&database_id) {
return Ok(editor.clone());
}
let mut editors = self.editors.write().await;
let database = MutexDatabase::new(self.with_user_database(
Err(FlowyError::record_not_found()),
|database| {
database
.get_database(&database_id)
.ok_or_else(FlowyError::record_not_found)
},
)?);
let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?);
editors.insert(database_id.to_string(), editor.clone());
Ok(editor)
}
#[tracing::instrument(level = "debug", skip_all)]
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
let database_id = self.with_user_database(None, |database| {
database.get_database_id_with_view_id(view_id)
});
if let Some(database_id) = database_id {
let mut editors = self.editors.write().await;
if let Some(editor) = editors.get(&database_id) {
if editor.close_view_editor(view_id).await {
editor.close().await;
editors.remove(&database_id);
}
}
}
Ok(())
}
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> {
let database_data = self.with_user_database(Err(FlowyError::internal()), |database| {
let data = database.get_database_duplicated_data(view_id)?;
let json_bytes = data.to_json_bytes()?;
Ok(json_bytes)
})?;
Ok(database_data)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn create_database_with_database_data(
&self,
view_id: &str,
data: Vec<u8>,
) -> FlowyResult<()> {
let mut database_data = DatabaseData::from_json_bytes(data)?;
database_data.view.id = view_id.to_string();
self.with_user_database(
Err(FlowyError::internal().context("Create database with data failed")),
|database| {
let database = database.create_database_with_data(database_data)?;
Ok(database)
},
)?;
Ok(())
}
pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> {
let _ = self.with_user_database(
Err(FlowyError::internal().context("Create database with params failed")),
|user_database| {
let database = user_database.create_database(params)?;
Ok(database)
},
)?;
Ok(())
}
pub async fn create_linked_view(
&self,
name: String,
layout: DatabaseLayoutPB,
database_id: String,
target_view_id: String,
duplicated_view_id: Option<String>,
) -> FlowyResult<()> {
self.with_user_database(
Err(FlowyError::internal().context("Create database view failed")),
|user_database| {
let database = user_database
.get_database(&database_id)
.ok_or_else(FlowyError::record_not_found)?;
match duplicated_view_id {
None => {
let params = CreateViewParams::new(database_id, target_view_id, name, layout.into());
database.create_linked_view(params);
},
Some(duplicated_view_id) => {
database.duplicate_linked_view(&duplicated_view_id);
},
}
Ok(())
},
)?;
Ok(())
}
fn with_user_database<F, Output>(&self, default_value: Output, f: F) -> Output
where
F: FnOnce(&InnerUserDatabase) -> Output,
{
let database = self.user_database.lock();
match &*database {
None => default_value,
Some(folder) => f(folder),
}
}
}
#[derive(Clone, Default)]
pub struct UserDatabase(Arc<Mutex<Option<InnerUserDatabase>>>);
impl Deref for UserDatabase {
type Target = Arc<Mutex<Option<InnerUserDatabase>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
unsafe impl Sync for UserDatabase {}
unsafe impl Send for UserDatabase {}

View File

@ -0,0 +1,55 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
const OBSERVABLE_CATEGORY: &str = "Grid";
#[derive(ProtoBuf_Enum, Debug)]
pub enum DatabaseNotification {
Unknown = 0,
/// Trigger after inserting/deleting/updating a row
DidUpdateViewRows = 20,
/// Trigger when the visibility of the row was changed. For example, updating the filter will trigger the notification
DidUpdateViewRowsVisibility = 21,
/// Trigger after inserting/deleting/updating a field
DidUpdateFields = 22,
/// Trigger after editing a cell
DidUpdateCell = 40,
/// Trigger after editing a field properties including rename,update type option, etc
DidUpdateField = 50,
/// Trigger after the number of groups is changed
DidUpdateGroups = 60,
/// Trigger after inserting/deleting/updating/moving a row
DidUpdateGroupRow = 61,
/// Trigger when setting a new grouping field
DidGroupByField = 62,
/// Trigger after inserting/deleting/updating a filter
DidUpdateFilter = 63,
/// Trigger after inserting/deleting/updating a sort
DidUpdateSort = 64,
/// Trigger after the sort configurations are changed
DidReorderRows = 65,
/// Trigger after editing the row that hit the sort rule
DidReorderSingleRow = 66,
/// Trigger when the settings of the database are changed
DidUpdateSettings = 70,
// Trigger when the layout setting of the database is updated
DidUpdateLayoutSettings = 80,
// Trigger when the layout field of the database is changed
DidSetNewLayoutField = 81,
}
impl std::default::Default for DatabaseNotification {
fn default() -> Self {
DatabaseNotification::Unknown
}
}
impl std::convert::From<DatabaseNotification> for i32 {
fn from(notification: DatabaseNotification) -> Self {
notification as i32
}
}
#[tracing::instrument(level = "trace")]
pub fn send_notification(id: &str, ty: DatabaseNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY)
}

View File

@ -0,0 +1,126 @@
use parking_lot::RwLock;
use std::any::{type_name, Any};
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::Arc;
pub type CellCache = Arc<RwLock<AnyTypeCache<u64>>>;
pub type CellFilterCache = Arc<RwLock<AnyTypeCache<String>>>;
#[derive(Default, Debug)]
/// The better option is use LRU cache
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

@ -0,0 +1,442 @@
use std::collections::HashMap;
use std::fmt::Debug;
use collab_database::fields::Field;
use collab_database::rows::{get_field_type_from_cell, Cell, Cells};
use flowy_error::{ErrorCode, FlowyResult};
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellProtobufBlob};
use crate::services::field::*;
use crate::services::group::make_no_status_group;
/// Decode the opaque cell data into readable format content
pub trait CellDataDecoder: TypeOption {
///
/// Tries to decode the opaque cell string to `decoded_field_type`'s cell data. Sometimes, the `field_type`
/// of the `FieldRevision` is not equal to the `decoded_field_type`(This happened When switching
/// the field type of the `FieldRevision` to another field type). So the cell data is need to do
/// some transformation.
///
/// For example, the current field type of the `FieldRevision` is a checkbox. When switching the field
/// type from the checkbox to single select, it will create two new options,`Yes` and `No`, if they don't exist.
/// But the data of the cell doesn't change. We can't iterate all the rows to transform the cell
/// data that can be parsed by the current field type. One approach is to transform the cell data
/// when it get read. For the moment, the cell data is a string, `Yes` or `No`. It needs to compare
/// with the option's name, if match return the id of the option.
fn decode_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData>;
/// Same as `decode_cell_data` does but Decode the cell data to readable `String`
/// For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma.
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String;
fn decode_cell_to_str(&self, cell: &Cell) -> String;
}
pub trait CellDataChangeset: TypeOption {
/// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset`
/// implements the `FromCellChangesetString` trait.
/// For example,the SelectOptionCellChangeset,DateCellChangeset. etc.
///
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)>;
}
/// changeset: It will be deserialized into specific data base on the FieldType.
/// For example,
/// FieldType::RichText => String
/// FieldType::SingleSelect => SelectOptionChangeset
///
/// cell_rev: It will be None if the cell does not contain any data.
pub fn apply_cell_data_changeset<C: ToCellChangeset>(
changeset: C,
cell: Option<Cell>,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Cell {
let changeset = changeset.to_cell_changeset_str();
let field_type = FieldType::from(field.field_type);
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
.get_type_option_cell_data_handler(&field_type)
{
None => Cell::default(),
Some(handler) => handler
.handle_cell_changeset(changeset, cell, field)
.unwrap_or_default(),
}
}
pub fn get_type_cell_protobuf(
cell: &Cell,
field: &Field,
cell_cache: Option<CellCache>,
) -> CellProtobufBlob {
let from_field_type = get_field_type_from_cell(cell);
if from_field_type.is_none() {
return CellProtobufBlob::default();
}
let from_field_type = from_field_type.unwrap();
let to_field_type = FieldType::from(field.field_type);
match try_decode_cell_str_to_cell_protobuf(
cell,
&from_field_type,
&to_field_type,
field,
cell_cache,
) {
Ok(cell_bytes) => cell_bytes,
Err(e) => {
tracing::error!("Decode cell data failed, {:?}", e);
CellProtobufBlob::default()
},
}
}
pub fn get_type_cell_data<Output>(
cell: &Cell,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Option<Output>
where
Output: Default + 'static,
{
let from_field_type = get_field_type_from_cell(cell)?;
let to_field_type = FieldType::from(field.field_type);
try_decode_cell_to_cell_data(
cell,
&from_field_type,
&to_field_type,
field,
cell_data_cache,
)
}
/// Decode the opaque cell data from one field type to another using the corresponding `TypeOption`
///
/// The cell data might become an empty string depends on the to_field_type's `TypeOption`
/// support transform the from_field_type's cell data or not.
///
/// # Arguments
///
/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the
/// `FromCellString` trait.
/// * `from_field_type`: the original field type of the passed-in cell data. Check the `TypeCellData`
/// that is used to save the origin field type of the cell data.
/// * `to_field_type`: decode the passed-in cell data to this field type. It will use the to_field_type's
/// TypeOption to decode this cell data.
/// * `field_rev`: used to get the corresponding TypeOption for the specified field type.
///
/// returns: CellBytes
///
pub fn try_decode_cell_str_to_cell_protobuf(
cell: &Cell,
from_field_type: &FieldType,
to_field_type: &FieldType,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> FlowyResult<CellProtobufBlob> {
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)
{
None => Ok(CellProtobufBlob::default()),
Some(handler) => handler.handle_cell_str(cell, from_field_type, field),
}
}
pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
cell: &Cell,
from_field_type: &FieldType,
to_field_type: &FieldType,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Option<T> {
let handler = TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)?;
handler
.get_cell_data(cell, from_field_type, field)
.ok()?
.unbox_or_none::<T>()
}
/// Returns a string that represents the current field_type's cell data.
/// For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma.
///
/// # Arguments
///
/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the
/// `FromCellString` trait.
/// * `decoded_field_type`: the field_type of the cell_str
/// * `field_type`: use this field type's `TypeOption` to stringify this cell_str
/// * `field_rev`: used to get the corresponding TypeOption for the specified field type.
///
/// returns: String
pub fn stringify_cell_data(
cell: &Cell,
decoded_field_type: &FieldType,
field_type: &FieldType,
field: &Field,
) -> String {
match TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(field_type)
{
None => "".to_string(),
Some(handler) => handler.stringify_cell_str(cell, decoded_field_type, field),
}
}
pub fn insert_text_cell(s: String, field: &Field) -> Cell {
apply_cell_data_changeset(s, None, field, None)
}
pub fn insert_number_cell(num: i64, field: &Field) -> Cell {
apply_cell_data_changeset(num.to_string(), None, field, None)
}
pub fn insert_url_cell(url: String, field: &Field) -> Cell {
// checking if url is equal to group id of no status group because everywhere
// except group of rows with empty url the group id is equal to the url
// so then on the case that url is equal to empty url group id we should change
// the url to empty string
let _no_status_group_id = make_no_status_group(field).id;
let url = match url {
a if a == _no_status_group_id => "".to_owned(),
_ => url,
};
apply_cell_data_changeset(url, None, field, None)
}
pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
let s = if is_check {
CHECK.to_string()
} else {
UNCHECK.to_string()
};
apply_cell_data_changeset(s, None, field, None)
}
pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell {
let cell_data = serde_json::to_string(&DateCellChangeset {
date: Some(timestamp.to_string()),
time: None,
include_time: Some(false),
is_utc: true,
})
.unwrap();
apply_cell_data_changeset(cell_data, None, field, None)
}
pub fn insert_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
let changeset =
SelectOptionCellChangeset::from_insert_options(option_ids).to_cell_changeset_str();
apply_cell_data_changeset(changeset, None, field, None)
}
pub fn delete_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
let changeset =
SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str();
apply_cell_data_changeset(changeset, None, field, None)
}
/// Deserialize the String into cell specific data type.
pub trait FromCellString {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized;
}
/// If the changeset applying to the cell is not String type, it should impl this trait.
/// Deserialize the string into cell specific changeset.
pub trait FromCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized;
}
impl FromCellChangeset for String {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
Ok(changeset)
}
}
pub trait ToCellChangeset: Debug {
fn to_cell_changeset_str(&self) -> String;
}
impl ToCellChangeset for String {
fn to_cell_changeset_str(&self) -> String {
self.clone()
}
}
pub struct AnyCellChangeset<T>(pub Option<T>);
impl<T> AnyCellChangeset<T> {
pub fn try_into_inner(self) -> FlowyResult<T> {
match self.0 {
None => Err(ErrorCode::InvalidData.into()),
Some(data) => Ok(data),
}
}
}
impl<T, C: ToString> std::convert::From<C> for AnyCellChangeset<T>
where
T: FromCellChangeset,
{
fn from(changeset: C) -> Self {
match T::from_changeset(changeset.to_string()) {
Ok(data) => AnyCellChangeset(Some(data)),
Err(e) => {
tracing::error!("Deserialize CellDataChangeset failed: {}", e);
AnyCellChangeset(None)
},
}
}
}
// impl std::convert::From<String> for AnyCellChangeset<String> {
// fn from(s: String) -> Self {
// AnyCellChangeset(Some(s))
// }
// }
pub struct CellBuilder {
cells: Cells,
field_maps: HashMap<String, Field>,
}
impl CellBuilder {
pub fn with_cells(cell_by_field_id: HashMap<String, String>, fields: Vec<Field>) -> Self {
let field_maps = fields
.into_iter()
.map(|field| (field.id.clone(), field))
.collect::<HashMap<String, Field>>();
let mut cells = Cells::new();
for (field_id, cell_str) in cell_by_field_id {
if let Some(field) = field_maps.get(&field_id) {
let field_type = FieldType::from(field.field_type);
match field_type {
FieldType::RichText => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
FieldType::Number => {
if let Ok(num) = cell_str.parse::<i64>() {
cells.insert(field_id, insert_number_cell(num, field));
}
},
FieldType::DateTime => {
if let Ok(timestamp) = cell_str.parse::<i64>() {
cells.insert(field_id, insert_date_cell(timestamp, field));
}
},
FieldType::SingleSelect | FieldType::MultiSelect => {
if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_str) {
cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field));
}
},
FieldType::Checkbox => {
if let Ok(value) = CheckboxCellData::from_cell_str(&cell_str) {
cells.insert(field_id, insert_checkbox_cell(value.into_inner(), field));
}
},
FieldType::URL => {
cells.insert(field_id, insert_url_cell(cell_str, field));
},
FieldType::Checklist => {
if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_str) {
cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field));
}
},
}
}
}
CellBuilder { cells, field_maps }
}
pub fn build(self) -> Cells {
self.cells
}
pub fn insert_text_cell(&mut self, field_id: &str, data: String) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the text field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_text_cell(data, field));
},
}
}
pub fn insert_url_cell(&mut self, field_id: &str, data: String) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the url field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_url_cell(data, field));
},
}
}
pub fn insert_number_cell(&mut self, field_id: &str, num: i64) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the number field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_number_cell(num, field));
},
}
}
pub fn insert_checkbox_cell(&mut self, field_id: &str, is_check: bool) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the checkbox field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_checkbox_cell(is_check, field));
},
}
}
pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the date field with id: {}", field_id),
Some(field) => {
self
.cells
.insert(field_id.to_owned(), insert_date_cell(timestamp, field));
},
}
}
pub fn insert_select_option_cell(&mut self, field_id: &str, option_ids: Vec<String>) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the select option field with id: {}", field_id),
Some(field) => {
self.cells.insert(
field_id.to_owned(),
insert_select_option_cell(option_ids, field),
);
},
}
}
}

View File

@ -0,0 +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

@ -0,0 +1,208 @@
use crate::entities::FieldType;
use bytes::Bytes;
use database_model::CellRevision;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
/// TypeCellData is a generic CellData, you can parse the type_cell_data according to the field_type.
/// The `data` is encoded by JSON format. You can use `IntoCellData` to decode the opaque data to
/// concrete cell type.
/// TypeCellData -> IntoCellData<T> -> T
///
/// The `TypeCellData` is the same as the cell data that was saved to disk except it carries the
/// field_type. The field_type indicates the cell data original `FieldType`. The field_type will
/// be changed if the current Field's type switch from one to another.
///
#[derive(Debug, Serialize, Deserialize)]
pub struct TypeCellData {
#[serde(rename = "data")]
pub cell_str: String,
pub field_type: FieldType,
}
impl TypeCellData {
pub fn from_field_type(field_type: &FieldType) -> TypeCellData {
Self {
cell_str: "".to_string(),
field_type: field_type.clone(),
}
}
pub fn from_json_str(s: &str) -> FlowyResult<Self> {
let type_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| {
let msg = format!("Deserialize {} to type cell data failed.{}", s, err);
FlowyError::internal().context(msg)
})?;
Ok(type_cell_data)
}
pub fn into_inner(self) -> String {
self.cell_str
}
}
impl std::convert::TryFrom<String> for TypeCellData {
type Error = FlowyError;
fn try_from(value: String) -> Result<Self, Self::Error> {
TypeCellData::from_json_str(&value)
}
}
impl ToString for TypeCellData {
fn to_string(&self) -> String {
self.cell_str.clone()
}
}
impl std::convert::TryFrom<&CellRevision> for TypeCellData {
type Error = FlowyError;
fn try_from(value: &CellRevision) -> Result<Self, Self::Error> {
Self::from_json_str(&value.type_cell_data)
}
}
impl std::convert::TryFrom<CellRevision> for TypeCellData {
type Error = FlowyError;
fn try_from(value: CellRevision) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}
impl TypeCellData {
pub fn new(cell_str: String, field_type: FieldType) -> Self {
TypeCellData {
cell_str,
field_type,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| "".to_owned())
}
pub fn is_number(&self) -> bool {
self.field_type == FieldType::Number
}
pub fn is_text(&self) -> bool {
self.field_type == FieldType::RichText
}
pub fn is_checkbox(&self) -> bool {
self.field_type == FieldType::Checkbox
}
pub fn is_date(&self) -> bool {
self.field_type == FieldType::DateTime
}
pub fn is_single_select(&self) -> bool {
self.field_type == FieldType::SingleSelect
}
pub fn is_multi_select(&self) -> bool {
self.field_type == FieldType::MultiSelect
}
pub fn is_checklist(&self) -> bool {
self.field_type == FieldType::Checklist
}
pub fn is_url(&self) -> bool {
self.field_type == FieldType::URL
}
pub fn is_select_option(&self) -> bool {
self.field_type == FieldType::MultiSelect || self.field_type == FieldType::SingleSelect
}
}
/// The data is encoded by protobuf or utf8. You should choose the corresponding decode struct to parse it.
///
/// For example:
///
/// * Use DateCellDataPB to parse the data when the FieldType is Date.
/// * Use URLCellDataPB to parse the data when the FieldType is URL.
/// * Use String to parse the data when the FieldType is RichText, Number, or Checkbox.
/// * Check out the implementation of CellDataOperation trait for more information.
#[derive(Default, Debug)]
pub struct CellProtobufBlob(pub Bytes);
pub trait DecodedCellData {
type Object;
fn is_empty(&self) -> bool;
}
pub trait CellProtobufBlobParser {
type Object: DecodedCellData;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object>;
}
pub trait CellStringParser {
type Object;
fn parser_cell_str(&self, s: &str) -> Option<Self::Object>;
}
pub trait CellBytesCustomParser {
type Object;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object>;
}
impl CellProtobufBlob {
pub fn new<T: AsRef<[u8]>>(data: T) -> Self {
let bytes = Bytes::from(data.as_ref().to_vec());
Self(bytes)
}
pub fn from<T: TryInto<Bytes>>(bytes: T) -> FlowyResult<Self>
where
<T as TryInto<Bytes>>::Error: std::fmt::Debug,
{
let bytes = bytes.try_into().map_err(internal_error)?;
Ok(Self(bytes))
}
pub fn parser<P>(&self) -> FlowyResult<P::Object>
where
P: CellProtobufBlobParser,
{
P::parser(&self.0)
}
pub fn custom_parser<P>(&self, parser: P) -> FlowyResult<P::Object>
where
P: CellBytesCustomParser,
{
parser.parse(&self.0)
}
// pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult<T>
// where
// <T as TryFrom<&'a [u8]>>::Error: std::fmt::Debug,
// {
// T::try_from(self.0.as_ref()).map_err(internal_error)
// }
}
impl ToString for CellProtobufBlob {
fn to_string(&self) -> String {
match String::from_utf8(self.0.to_vec()) {
Ok(s) => s,
Err(e) => {
tracing::error!("DecodedCellData to string failed: {:?}", e);
"".to_string()
},
}
}
}
impl std::ops::Deref for CellProtobufBlob {
type Target = Bytes;
fn deref(&self) -> &Self::Target {
&self.0
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
use collab_database::views::RowOrder;
#[derive(Debug, Clone)]
pub enum DatabaseRowEvent {
InsertRow(InsertedRow),
UpdateRow(UpdatedRow),
DeleteRow(i64),
Move {
deleted_row_id: i64,
inserted_row: InsertedRow,
},
}
#[derive(Debug, Clone)]
pub struct InsertedRow {
pub row: RowOrder,
pub index: Option<i32>,
pub is_new: bool,
}
#[derive(Debug, Clone)]
pub struct UpdatedRow {
pub row: RowOrder,
// represents as the cells that were updated in this row.
pub field_ids: Vec<String>,
}

View File

@ -0,0 +1,7 @@
mod database_editor;
mod entities;
mod util;
pub use database_editor::*;
pub use entities::*;
pub(crate) use util::database_view_setting_pb_from_view;

View File

@ -0,0 +1,56 @@
use crate::entities::{
CalendarLayoutSettingPB, DatabaseLayoutPB, DatabaseViewSettingPB, FilterPB, GroupSettingPB,
LayoutSettingPB, SortPB,
};
use crate::services::filter::Filter;
use crate::services::group::GroupSetting;
use crate::services::setting::CalendarLayoutSetting;
use crate::services::sort::Sort;
use collab_database::views::DatabaseView;
pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> DatabaseViewSettingPB {
let layout_setting = if let Some(layout_setting) = view.layout_settings.get(&view.layout) {
let calendar_setting =
CalendarLayoutSettingPB::from(CalendarLayoutSetting::from(layout_setting.clone()));
LayoutSettingPB {
calendar: Some(calendar_setting),
}
} else {
LayoutSettingPB::default()
};
let current_layout: DatabaseLayoutPB = view.layout.into();
let filters = view
.filters
.into_iter()
.flat_map(|value| match Filter::try_from(value) {
Ok(filter) => Some(FilterPB::from(&filter)),
Err(_) => None,
})
.collect::<Vec<FilterPB>>();
let group_settings = view
.group_settings
.into_iter()
.flat_map(|value| match GroupSetting::try_from(value) {
Ok(setting) => Some(GroupSettingPB::from(&setting)),
Err(_) => None,
})
.collect::<Vec<GroupSettingPB>>();
let sorts = view
.sorts
.into_iter()
.flat_map(|value| match Sort::try_from(value) {
Ok(sort) => Some(SortPB::from(&sort)),
Err(_) => None,
})
.collect::<Vec<SortPB>>();
DatabaseViewSettingPB {
current_layout,
filters: filters.into(),
group_settings: group_settings.into(),
sorts: sorts.into(),
layout_setting,
}
}

View File

@ -0,0 +1,11 @@
mod notifier;
mod view_editor;
mod view_filter;
mod view_group;
mod view_sort;
mod views;
// mod trait_impl;
pub use notifier::*;
pub use view_editor::*;
pub use views::*;

View File

@ -0,0 +1,111 @@
#![allow(clippy::while_let_loop)]
use crate::entities::{
DatabaseViewSettingPB, FilterChangesetNotificationPB, GroupChangesetPB, GroupRowsNotificationPB,
ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangesetPB, SortChangesetNotificationPB,
};
use crate::notification::{send_notification, DatabaseNotification};
use crate::services::filter::FilterResultNotification;
use crate::services::sort::{ReorderAllRowsResult, ReorderSingleRowResult};
use async_stream::stream;
use futures::stream::StreamExt;
use tokio::sync::broadcast;
#[derive(Clone)]
pub enum DatabaseViewChanged {
FilterNotification(FilterResultNotification),
ReorderAllRowsNotification(ReorderAllRowsResult),
ReorderSingleRowNotification(ReorderSingleRowResult),
}
pub type DatabaseViewChangedNotifier = broadcast::Sender<DatabaseViewChanged>;
pub(crate) struct DatabaseViewChangedReceiverRunner(
pub(crate) Option<broadcast::Receiver<DatabaseViewChanged>>,
);
impl DatabaseViewChangedReceiverRunner {
pub(crate) async fn run(mut self) {
let mut receiver = self.0.take().expect("Only take once");
let stream = stream! {
loop {
match receiver.recv().await {
Ok(changed) => yield changed,
Err(_e) => break,
}
}
};
stream
.for_each(|changed| async {
match changed {
DatabaseViewChanged::FilterNotification(notification) => {
let changeset = RowsVisibilityChangesetPB {
view_id: notification.view_id,
visible_rows: notification.visible_rows,
invisible_rows: notification.invisible_rows,
};
send_notification(
&changeset.view_id,
DatabaseNotification::DidUpdateViewRowsVisibility,
)
.payload(changeset)
.send()
},
DatabaseViewChanged::ReorderAllRowsNotification(notification) => {
let row_orders = ReorderAllRowsPB {
row_orders: notification.row_orders,
};
send_notification(&notification.view_id, DatabaseNotification::DidReorderRows)
.payload(row_orders)
.send()
},
DatabaseViewChanged::ReorderSingleRowNotification(notification) => {
let reorder_row = ReorderSingleRowPB {
row_id: notification.row_id,
old_index: notification.old_index as i32,
new_index: notification.new_index as i32,
};
send_notification(
&notification.view_id,
DatabaseNotification::DidReorderSingleRow,
)
.payload(reorder_row)
.send()
},
}
})
.await;
}
}
pub async fn notify_did_update_group_rows(payload: GroupRowsNotificationPB) {
send_notification(&payload.group_id, DatabaseNotification::DidUpdateGroupRow)
.payload(payload)
.send();
}
pub async fn notify_did_update_filter(notification: FilterChangesetNotificationPB) {
send_notification(&notification.view_id, DatabaseNotification::DidUpdateFilter)
.payload(notification)
.send();
}
pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) {
if !notification.is_empty() {
send_notification(&notification.view_id, DatabaseNotification::DidUpdateSort)
.payload(notification)
.send();
}
}
pub(crate) async fn notify_did_update_groups(view_id: &str, changeset: GroupChangesetPB) {
send_notification(view_id, DatabaseNotification::DidUpdateGroups)
.payload(changeset)
.send();
}
pub(crate) async fn notify_did_update_setting(view_id: &str, setting: DatabaseViewSettingPB) {
send_notification(view_id, DatabaseNotification::DidUpdateSettings)
.payload(setting)
.send();
}

View File

@ -0,0 +1,791 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use collab_database::database::{gen_database_filter_id, gen_database_sort_id};
use collab_database::fields::Field;
use collab_database::rows::{Cells, Row, RowCell, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
use tokio::sync::{broadcast, RwLock};
use flowy_error::{FlowyError, FlowyResult};
use flowy_task::TaskDispatcher;
use lib_infra::future::Fut;
use crate::entities::{
AlterFilterParams, AlterSortParams, CalendarEventPB, DeleteFilterParams, DeleteGroupParams,
DeleteSortParams, FieldType, GroupChangesetPB, GroupPB, GroupRowsNotificationPB,
InsertGroupParams, InsertedGroupPB, InsertedRowPB, LayoutSettingPB, LayoutSettingParams, RowPB,
RowsChangesetPB, SortChangesetNotificationPB, SortPB,
};
use crate::notification::{send_notification, DatabaseNotification};
use crate::services::cell::CellCache;
use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent};
use crate::services::database_view::view_filter::make_filter_controller;
use crate::services::database_view::view_group::{
get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field,
};
use crate::services::database_view::view_sort::make_sort_controller;
use crate::services::database_view::{
notify_did_update_filter, notify_did_update_group_rows, notify_did_update_groups,
notify_did_update_setting, notify_did_update_sort, DatabaseViewChangedNotifier,
DatabaseViewChangedReceiverRunner,
};
use crate::services::field::TypeOptionCellDataHandler;
use crate::services::filter::{
Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType,
};
use crate::services::group::{GroupController, GroupSetting, MoveGroupRowContext, RowChangeset};
use crate::services::setting::CalendarLayoutSetting;
use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType};
pub trait DatabaseViewData: Send + Sync + 'static {
fn get_view_setting(&self, view_id: &str) -> Fut<Option<DatabaseView>>;
/// If the field_ids is None, then it will return all the field revisions
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>>;
/// Returns the field with the field_id
fn get_field(&self, field_id: &str) -> Fut<Option<Arc<Field>>>;
fn get_primary_field(&self) -> Fut<Option<Arc<Field>>>;
/// Returns the index of the row with row_id
fn index_of_row(&self, view_id: &str, row_id: RowId) -> Fut<Option<usize>>;
/// Returns the `index` and `RowRevision` with row_id
fn get_row(&self, view_id: &str, row_id: RowId) -> Fut<Option<(usize, Arc<Row>)>>;
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>>;
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>;
fn get_cell_in_row(&self, field_id: &str, row_id: RowId) -> Fut<Option<Arc<RowCell>>>;
fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout;
fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting>;
fn insert_group_setting(&self, view_id: &str, setting: GroupSetting);
fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Sort>;
fn insert_sort(&self, view_id: &str, sort: Sort);
fn remove_sort(&self, view_id: &str, sort_id: &str);
fn get_all_sorts(&self, view_id: &str) -> Vec<Sort>;
fn remove_all_sorts(&self, view_id: &str);
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>>;
fn delete_filter(&self, view_id: &str, filter_id: &str);
fn insert_filter(&self, view_id: &str, filter: Filter);
fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter>;
fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option<Filter>;
fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting>;
fn insert_layout_setting(
&self,
view_id: &str,
layout_ty: &DatabaseLayout,
layout_setting: LayoutSetting,
);
/// Returns a `TaskDispatcher` used to poll a `Task`
fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>>;
fn get_type_option_cell_handler(
&self,
field: &Field,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>>;
}
pub struct DatabaseViewEditor {
pub view_id: String,
delegate: Arc<dyn DatabaseViewData>,
group_controller: Arc<RwLock<Box<dyn GroupController>>>,
filter_controller: Arc<FilterController>,
sort_controller: Arc<RwLock<SortController>>,
pub notifier: DatabaseViewChangedNotifier,
}
impl Drop for DatabaseViewEditor {
fn drop(&mut self) {
tracing::trace!("Drop {}", std::any::type_name::<Self>());
}
}
impl DatabaseViewEditor {
pub async fn new(
view_id: String,
delegate: Arc<dyn DatabaseViewData>,
cell_cache: CellCache,
) -> FlowyResult<Self> {
let (notifier, _) = broadcast::channel(100);
tokio::spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run());
let group_controller = new_group_controller(view_id.clone(), delegate.clone()).await?;
let group_controller = Arc::new(RwLock::new(group_controller));
let filter_controller = make_filter_controller(
&view_id,
delegate.clone(),
notifier.clone(),
cell_cache.clone(),
)
.await;
let sort_controller = make_sort_controller(
&view_id,
delegate.clone(),
notifier.clone(),
filter_controller.clone(),
cell_cache,
)
.await;
Ok(Self {
view_id,
delegate,
group_controller,
filter_controller,
sort_controller,
notifier,
})
}
pub async fn close(&self) {
self.sort_controller.write().await.close().await;
self.filter_controller.close().await;
}
pub async fn v_will_create_row(&self, cells: &mut Cells, group_id: &Option<String>) {
if group_id.is_none() {
return;
}
let group_id = group_id.as_ref().unwrap();
let _ = self
.mut_group_controller(|group_controller, field| {
group_controller.will_create_row(cells, &field, group_id);
Ok(())
})
.await;
}
pub async fn v_did_create_row(&self, row: &Row, group_id: &Option<String>, index: usize) {
// Send the group notification if the current view has groups
match group_id.as_ref() {
None => {},
Some(group_id) => {
self
.group_controller
.write()
.await
.did_create_row(row, group_id);
let inserted_row = InsertedRowPB {
row: RowPB::from(row),
index: Some(index as i32),
is_new: true,
};
let changeset = GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row]);
notify_did_update_group_rows(changeset).await;
},
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn v_did_delete_row(&self, row: &Row) {
// Send the group notification if the current view has groups;
let result = self
.mut_group_controller(|group_controller, field| {
group_controller.did_delete_delete_row(row, &field)
})
.await;
if let Some(result) = result {
tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets);
for changeset in result.row_changesets {
notify_did_update_group_rows(changeset).await;
}
}
}
pub async fn v_did_update_row(&self, old_row: &Option<Row>, row: &Row) {
let result = self
.mut_group_controller(|group_controller, field| {
Ok(group_controller.did_update_group_row(old_row, row, &field))
})
.await;
if let Some(Ok(result)) = result {
let mut changeset = GroupChangesetPB {
view_id: self.view_id.clone(),
..Default::default()
};
if let Some(inserted_group) = result.inserted_group {
tracing::trace!("Create group after editing the row: {:?}", inserted_group);
changeset.inserted_groups.push(inserted_group);
}
if let Some(delete_group) = result.deleted_group {
tracing::trace!("Delete group after editing the row: {:?}", delete_group);
changeset.deleted_groups.push(delete_group.group_id);
}
notify_did_update_groups(&self.view_id, changeset).await;
tracing::trace!(
"Group changesets after editing the row: {:?}",
result.row_changesets
);
for changeset in result.row_changesets {
notify_did_update_group_rows(changeset).await;
}
}
let filter_controller = self.filter_controller.clone();
let sort_controller = self.sort_controller.clone();
let row_id = row.id;
tokio::spawn(async move {
filter_controller.did_receive_row_changed(row_id).await;
sort_controller
.read()
.await
.did_receive_row_changed(row_id)
.await;
});
}
pub async fn v_filter_rows(&self, rows: &mut Vec<Arc<Row>>) {
self.filter_controller.filter_rows(rows).await
}
pub async fn v_sort_rows(&self, rows: &mut Vec<Arc<Row>>) {
self.sort_controller.write().await.sort_rows(rows).await
}
pub async fn v_get_rows(&self) -> Vec<Arc<Row>> {
let mut rows = self.delegate.get_rows(&self.view_id).await;
self.v_filter_rows(&mut rows).await;
self.v_sort_rows(&mut rows).await;
rows
}
pub async fn v_move_group_row(
&self,
row: &Row,
row_changeset: &mut RowChangeset,
to_group_id: &str,
to_row_id: Option<RowId>,
) {
let result = self
.mut_group_controller(|group_controller, field| {
let move_row_context = MoveGroupRowContext {
row,
row_changeset,
field: field.as_ref(),
to_group_id,
to_row_id,
};
group_controller.move_group_row(move_row_context)
})
.await;
if let Some(result) = result {
let mut changeset = GroupChangesetPB {
view_id: self.view_id.clone(),
..Default::default()
};
if let Some(delete_group) = result.deleted_group {
tracing::info!("Delete group after moving the row: {:?}", delete_group);
changeset.deleted_groups.push(delete_group.group_id);
}
notify_did_update_groups(&self.view_id, changeset).await;
for changeset in result.row_changesets {
notify_did_update_group_rows(changeset).await;
}
}
}
/// Only call once after database view editor initialized
#[tracing::instrument(level = "trace", skip(self))]
pub async fn v_load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = self
.group_controller
.read()
.await
.groups()
.into_iter()
.map(|group_data| GroupPB::from(group_data.clone()))
.collect::<Vec<_>>();
tracing::trace!("Number of groups: {}", groups.len());
Ok(groups)
}
#[tracing::instrument(level = "trace", skip(self))]
pub async fn v_get_group(&self, group_id: &str) -> FlowyResult<GroupPB> {
match self.group_controller.read().await.get_group(group_id) {
None => Err(FlowyError::record_not_found().context("Can't find the group")),
Some((_, group)) => Ok(GroupPB::from(group)),
}
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_move_group(&self, from_group: &str, to_group: &str) -> FlowyResult<()> {
self
.group_controller
.write()
.await
.move_group(from_group, to_group)?;
match self.group_controller.read().await.get_group(from_group) {
None => tracing::warn!("Can not find the group with id: {}", from_group),
Some((index, group)) => {
let inserted_group = InsertedGroupPB {
group: GroupPB::from(group),
index: index as i32,
};
let changeset = GroupChangesetPB {
view_id: self.view_id.clone(),
inserted_groups: vec![inserted_group],
deleted_groups: vec![from_group.to_string()],
update_groups: vec![],
initial_groups: vec![],
};
notify_did_update_groups(&self.view_id, changeset).await;
},
}
Ok(())
}
pub async fn group_id(&self) -> String {
self.group_controller.read().await.field_id().to_string()
}
pub async fn v_initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
if self.group_controller.read().await.field_id() != params.field_id {
self.v_update_group_setting(&params.field_id).await?;
if let Some(view) = self.delegate.get_view_setting(&self.view_id).await {
let setting = database_view_setting_pb_from_view(view);
notify_did_update_setting(&self.view_id, setting).await;
}
}
Ok(())
}
pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> {
Ok(())
}
pub async fn v_get_all_sorts(&self) -> Vec<Sort> {
self.delegate.get_all_sorts(&self.view_id)
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_insert_sort(&self, params: AlterSortParams) -> FlowyResult<Sort> {
let is_exist = params.sort_id.is_some();
let sort_id = match params.sort_id {
None => gen_database_sort_id(),
Some(sort_id) => sort_id,
};
let sort = Sort {
id: sort_id,
field_id: params.field_id.clone(),
field_type: params.field_type,
condition: params.condition,
};
let sort_type = SortType::from(&sort);
let mut sort_controller = self.sort_controller.write().await;
self.delegate.insert_sort(&self.view_id, sort.clone());
let changeset = if is_exist {
sort_controller
.did_receive_changes(SortChangeset::from_update(sort_type))
.await
} else {
sort_controller
.did_receive_changes(SortChangeset::from_insert(sort_type))
.await
};
drop(sort_controller);
notify_did_update_sort(changeset).await;
Ok(sort)
}
pub async fn v_delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> {
let notification = self
.sort_controller
.write()
.await
.did_receive_changes(SortChangeset::from_delete(DeletedSortType::from(
params.clone(),
)))
.await;
self.delegate.remove_sort(&self.view_id, &params.sort_id);
notify_did_update_sort(notification).await;
Ok(())
}
pub async fn v_delete_all_sorts(&self) -> FlowyResult<()> {
let all_sorts = self.v_get_all_sorts().await;
self.delegate.remove_all_sorts(&self.view_id);
let mut notification = SortChangesetNotificationPB::new(self.view_id.clone());
notification.delete_sorts = all_sorts.into_iter().map(SortPB::from).collect();
notify_did_update_sort(notification).await;
Ok(())
}
pub async fn v_get_all_filters(&self) -> Vec<Arc<Filter>> {
self.delegate.get_all_filters(&self.view_id)
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_insert_filter(&self, params: AlterFilterParams) -> FlowyResult<()> {
let is_exist = params.filter_id.is_some();
let filter_id = match params.filter_id {
None => gen_database_filter_id(),
Some(filter_id) => filter_id,
};
let filter = Filter {
id: filter_id.clone(),
field_id: params.field_id.clone(),
field_type: params.field_type,
condition: params.condition,
content: params.content,
};
let filter_type = FilterType::from(&filter);
let filter_controller = self.filter_controller.clone();
let changeset = if is_exist {
let old_filter_type = self
.delegate
.get_filter(&self.view_id, &filter.id)
.map(|field| FilterType::from(&field));
self.delegate.insert_filter(&self.view_id, filter);
filter_controller
.did_receive_changes(FilterChangeset::from_update(UpdatedFilterType::new(
old_filter_type,
filter_type,
)))
.await
} else {
self.delegate.insert_filter(&self.view_id, filter);
filter_controller
.did_receive_changes(FilterChangeset::from_insert(filter_type))
.await
};
drop(filter_controller);
if let Some(changeset) = changeset {
notify_did_update_filter(changeset).await;
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
let filter_type = params.filter_type;
let changeset = self
.filter_controller
.did_receive_changes(FilterChangeset::from_delete(filter_type.clone()))
.await;
self
.delegate
.delete_filter(&self.view_id, &filter_type.filter_id);
if changeset.is_some() {
notify_did_update_filter(changeset.unwrap()).await;
}
Ok(())
}
pub async fn v_get_filter(&self, filter_id: &str) -> Option<Filter> {
self.delegate.get_filter(&self.view_id, filter_id)
}
/// Returns the current calendar settings
#[tracing::instrument(level = "debug", skip(self))]
pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams {
let mut layout_setting = LayoutSettingParams::default();
match layout_ty {
DatabaseLayout::Grid => {},
DatabaseLayout::Board => {},
DatabaseLayout::Calendar => {
if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) {
let calendar_setting = CalendarLayoutSetting::from(value);
// Check the field exist or not
if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await {
let field_type = FieldType::from(field.field_type);
// Check the type of field is Datetime or not
if field_type == FieldType::DateTime {
layout_setting.calendar = Some(calendar_setting);
}
}
}
},
}
tracing::debug!("{:?}", layout_setting);
layout_setting
}
/// Update the calendar settings and send the notification to refresh the UI
pub async fn v_set_layout_settings(
&self,
_layout_ty: &DatabaseLayout,
params: LayoutSettingParams,
) -> FlowyResult<()> {
// Maybe it needs no send notification to refresh the UI
if let Some(new_calendar_setting) = params.calendar {
if let Some(field) = self
.delegate
.get_field(&new_calendar_setting.field_id)
.await
{
let field_type = FieldType::from(field.field_type);
if field_type != FieldType::DateTime {
return Err(FlowyError::unexpect_calendar_field_type());
}
let layout_ty = DatabaseLayout::Calendar;
let old_calender_setting = self.v_get_layout_settings(&layout_ty).await.calendar;
self.delegate.insert_layout_setting(
&self.view_id,
&layout_ty,
new_calendar_setting.clone().into(),
);
let new_field_id = new_calendar_setting.field_id.clone();
let layout_setting_pb: LayoutSettingPB = LayoutSettingParams {
calendar: Some(new_calendar_setting),
}
.into();
if let Some(old_calendar_setting) = old_calender_setting {
// compare the new layout field id is equal to old layout field id
// if not equal, send the DidSetNewLayoutField notification
// if equal, send the DidUpdateLayoutSettings notification
if old_calendar_setting.field_id != new_field_id {
send_notification(&self.view_id, DatabaseNotification::DidSetNewLayoutField)
.payload(layout_setting_pb)
.send();
} else {
send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings)
.payload(layout_setting_pb)
.send();
}
} else {
tracing::warn!("Calendar setting should not be empty")
}
}
}
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn v_did_update_field_type_option(
&self,
field_id: &str,
_old_field: &Field,
) -> FlowyResult<()> {
if let Some(field) = self.delegate.get_field(field_id).await {
self
.sort_controller
.read()
.await
.did_update_view_field_type_option(&field)
.await;
// let filter = self
// .delegate
// .get_filter_by_field_id(&self.view_id, field_id);
//
// let old = old_field.map(|old_field| FilterType::from(filter));
// let new = FilterType::from(field.as_ref());
// let filter_type = UpdatedFilterType::new(old, new);
// let filter_changeset = FilterChangeset::from_update(filter_type);
// let filter_controller = self.filter_controller.clone();
// let _ = tokio::spawn(async move {
// if let Some(notification) = filter_controller
// .did_receive_changes(filter_changeset)
// .await
// {
// send_notification(&notification.view_id, DatabaseNotification::DidUpdateFilter)
// .payload(notification)
// .send();
// }
// });
}
Ok(())
}
///
///
/// # Arguments
///
/// * `field_id`:
///
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn v_update_group_setting(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field) = self.delegate.get_field(field_id).await {
let new_group_controller =
new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?;
let new_groups = new_group_controller
.groups()
.into_iter()
.map(|group| GroupPB::from(group.clone()))
.collect();
*self.group_controller.write().await = new_group_controller;
let changeset = GroupChangesetPB {
view_id: self.view_id.clone(),
initial_groups: new_groups,
..Default::default()
};
debug_assert!(!changeset.is_empty());
if !changeset.is_empty() {
send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField)
.payload(changeset)
.send();
}
}
Ok(())
}
pub async fn v_get_calendar_event(&self, row_id: RowId) -> Option<CalendarEventPB> {
let layout_ty = DatabaseLayout::Calendar;
let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?;
// Text
let primary_field = self.delegate.get_primary_field().await?;
let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, row_id).await?;
// Date
let date_field = self.delegate.get_field(&calendar_setting.field_id).await?;
let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, row_id).await?;
let title = text_cell
.into_text_field_cell_data()
.unwrap_or_default()
.into();
let timestamp = date_cell
.into_date_field_cell_data()
.unwrap_or_default()
.timestamp
.unwrap_or_default();
Some(CalendarEventPB {
row_id: row_id.into(),
title_field_id: primary_field.id.clone(),
title,
timestamp,
})
}
pub async fn v_get_all_calendar_events(&self) -> Option<Vec<CalendarEventPB>> {
let layout_ty = DatabaseLayout::Calendar;
let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?;
// Text
let primary_field = self.delegate.get_primary_field().await?;
let text_cells =
get_cells_for_field(self.delegate.clone(), &self.view_id, &primary_field.id).await;
// Date
let timestamp_by_row_id = get_cells_for_field(
self.delegate.clone(),
&self.view_id,
&calendar_setting.field_id,
)
.await
.into_iter()
.map(|date_cell| {
let row_id = date_cell.row_id;
// timestamp
let timestamp = date_cell
.into_date_field_cell_data()
.map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default())
.unwrap_or_default();
(row_id, timestamp)
})
.collect::<HashMap<RowId, i64>>();
let mut events: Vec<CalendarEventPB> = vec![];
for text_cell in text_cells {
let title_field_id = text_cell.field_id.clone();
let row_id = text_cell.row_id;
let timestamp = timestamp_by_row_id
.get(&row_id)
.cloned()
.unwrap_or_default();
let title = text_cell
.into_text_field_cell_data()
.unwrap_or_default()
.into();
let event = CalendarEventPB {
row_id: row_id.into(),
title_field_id,
title,
timestamp,
};
events.push(event);
}
Some(events)
}
pub async fn handle_block_event(&self, event: Cow<'_, DatabaseRowEvent>) {
let changeset = match event.into_owned() {
DatabaseRowEvent::InsertRow(row) => {
RowsChangesetPB::from_insert(self.view_id.clone(), vec![row.into()])
},
DatabaseRowEvent::UpdateRow(row) => {
RowsChangesetPB::from_update(self.view_id.clone(), vec![row.into()])
},
DatabaseRowEvent::DeleteRow(row_id) => {
RowsChangesetPB::from_delete(self.view_id.clone(), vec![row_id])
},
DatabaseRowEvent::Move {
deleted_row_id,
inserted_row,
} => RowsChangesetPB::from_move(
self.view_id.clone(),
vec![deleted_row_id],
vec![inserted_row.into()],
),
};
send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
.payload(changeset)
.send();
}
async fn mut_group_controller<F, T>(&self, f: F) -> Option<T>
where
F: FnOnce(&mut Box<dyn GroupController>, Arc<Field>) -> FlowyResult<T>,
{
let group_field_id = self.group_controller.read().await.field_id().to_owned();
match self.delegate.get_field(&group_field_id).await {
None => None,
Some(field) => {
let mut write_guard = self.group_controller.write().await;
f(&mut write_guard, field).ok()
},
}
}
}

View File

@ -0,0 +1,66 @@
use crate::services::cell::CellCache;
use crate::services::database_view::{
gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData,
};
use crate::services::filter::{Filter, FilterController, FilterDelegate, FilterTaskHandler};
use collab_database::fields::Field;
use collab_database::rows::{Row, RowId};
use lib_infra::future::{to_fut, Fut};
use std::sync::Arc;
pub async fn make_filter_controller(
view_id: &str,
delegate: Arc<dyn DatabaseViewData>,
notifier: DatabaseViewChangedNotifier,
cell_cache: CellCache,
) -> Arc<FilterController> {
let filters = delegate.get_all_filters(view_id);
let task_scheduler = delegate.get_task_scheduler();
let filter_delegate = DatabaseViewFilterDelegateImpl(delegate.clone());
let handler_id = gen_handler_id();
let filter_controller = FilterController::new(
view_id,
&handler_id,
filter_delegate,
task_scheduler.clone(),
filters,
cell_cache,
notifier,
)
.await;
let filter_controller = Arc::new(filter_controller);
task_scheduler
.write()
.await
.register_handler(FilterTaskHandler::new(
handler_id,
filter_controller.clone(),
));
filter_controller
}
struct DatabaseViewFilterDelegateImpl(Arc<dyn DatabaseViewData>);
impl FilterDelegate for DatabaseViewFilterDelegateImpl {
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>> {
let filter = self.0.get_filter(view_id, filter_id).map(Arc::new);
to_fut(async move { filter })
}
fn get_field(&self, field_id: &str) -> Fut<Option<Arc<Field>>> {
self.0.get_field(field_id)
}
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>> {
self.0.get_fields(view_id, field_ids)
}
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>> {
self.0.get_rows(view_id)
}
fn get_row(&self, view_id: &str, row_id: RowId) -> Fut<Option<(usize, Arc<Row>)>> {
self.0.get_row(view_id, row_id)
}
}

View File

@ -0,0 +1,150 @@
use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::rows::RowId;
use flowy_error::FlowyResult;
use lib_infra::future::{to_fut, Fut};
use crate::entities::FieldType;
use crate::services::database_view::DatabaseViewData;
use crate::services::field::RowSingleCellData;
use crate::services::group::{
find_new_grouping_field, make_group_controller, GroupController, GroupSetting,
GroupSettingReader, GroupSettingWriter,
};
pub async fn new_group_controller_with_field(
view_id: String,
delegate: Arc<dyn DatabaseViewData>,
grouping_field: Arc<Field>,
) -> FlowyResult<Box<dyn GroupController>> {
let setting_reader = GroupSettingReaderImpl(delegate.clone());
let rows = delegate.get_rows(&view_id).await;
let setting_writer = GroupSettingWriterImpl(delegate.clone());
make_group_controller(
view_id,
grouping_field,
rows,
setting_reader,
setting_writer,
)
.await
}
pub async fn new_group_controller(
view_id: String,
delegate: Arc<dyn DatabaseViewData>,
) -> FlowyResult<Box<dyn GroupController>> {
let setting_reader = GroupSettingReaderImpl(delegate.clone());
let setting_writer = GroupSettingWriterImpl(delegate.clone());
let fields = delegate.get_fields(&view_id, None).await;
let rows = delegate.get_rows(&view_id).await;
let layout = delegate.get_layout_for_view(&view_id);
// Read the grouping field or find a new grouping field
let grouping_field = setting_reader
.get_group_setting(&view_id)
.await
.and_then(|setting| {
fields
.iter()
.find(|field| field.id == setting.field_id)
.cloned()
})
.unwrap_or_else(|| find_new_grouping_field(&fields, &layout).unwrap());
make_group_controller(
view_id,
grouping_field,
rows,
setting_reader,
setting_writer,
)
.await
}
pub(crate) struct GroupSettingReaderImpl(pub Arc<dyn DatabaseViewData>);
impl GroupSettingReader for GroupSettingReaderImpl {
fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>> {
let mut settings = self.0.get_group_setting(view_id);
to_fut(async move {
if settings.is_empty() {
None
} else {
Some(Arc::new(settings.remove(0)))
}
})
}
fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>> {
let field_id = field_id.to_owned();
let view_id = view_id.to_owned();
let delegate = self.0.clone();
to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await })
}
}
pub(crate) async fn get_cell_for_row(
delegate: Arc<dyn DatabaseViewData>,
field_id: &str,
row_id: RowId,
) -> Option<RowSingleCellData> {
let field = delegate.get_field(field_id).await?;
let cell = delegate.get_cell_in_row(field_id, row_id).await?;
let field_type = FieldType::from(field.field_type);
if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) {
return match handler.get_cell_data(&cell, &field_type, &field) {
Ok(cell_data) => Some(RowSingleCellData {
row_id: cell.row_id,
field_id: field.id.clone(),
field_type: field_type.clone(),
cell_data,
}),
Err(_) => None,
};
}
None
}
// Returns the list of cells corresponding to the given field.
pub(crate) async fn get_cells_for_field(
delegate: Arc<dyn DatabaseViewData>,
view_id: &str,
field_id: &str,
) -> Vec<RowSingleCellData> {
if let Some(field) = delegate.get_field(field_id).await {
let field_type = FieldType::from(field.field_type);
if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) {
let cells = delegate.get_cells_for_field(view_id, field_id).await;
return cells
.iter()
.flat_map(
|cell| match handler.get_cell_data(cell, &field_type, &field) {
Ok(cell_data) => Some(RowSingleCellData {
row_id: cell.row_id,
field_id: field.id.clone(),
field_type: field_type.clone(),
cell_data,
}),
Err(_) => None,
},
)
.collect();
}
}
vec![]
}
struct GroupSettingWriterImpl(Arc<dyn DatabaseViewData>);
impl GroupSettingWriter for GroupSettingWriterImpl {
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> {
self.0.insert_group_setting(view_id, group_setting);
to_fut(async move { Ok(()) })
}
}

View File

@ -0,0 +1,77 @@
use crate::services::cell::CellCache;
use crate::services::database_view::{
gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData,
};
use crate::services::filter::FilterController;
use crate::services::sort::{Sort, SortController, SortDelegate, SortTaskHandler};
use collab_database::fields::Field;
use collab_database::rows::Row;
use lib_infra::future::{to_fut, Fut};
use std::sync::Arc;
use tokio::sync::RwLock;
pub(crate) async fn make_sort_controller(
view_id: &str,
delegate: Arc<dyn DatabaseViewData>,
notifier: DatabaseViewChangedNotifier,
filter_controller: Arc<FilterController>,
cell_cache: CellCache,
) -> Arc<RwLock<SortController>> {
let handler_id = gen_handler_id();
let sorts = delegate
.get_all_sorts(view_id)
.into_iter()
.map(Arc::new)
.collect();
let task_scheduler = delegate.get_task_scheduler();
let sort_delegate = DatabaseViewSortDelegateImpl {
delegate,
filter_controller,
};
let sort_controller = Arc::new(RwLock::new(SortController::new(
view_id,
&handler_id,
sorts,
sort_delegate,
task_scheduler.clone(),
cell_cache,
notifier,
)));
task_scheduler
.write()
.await
.register_handler(SortTaskHandler::new(handler_id, sort_controller.clone()));
sort_controller
}
struct DatabaseViewSortDelegateImpl {
delegate: Arc<dyn DatabaseViewData>,
filter_controller: Arc<FilterController>,
}
impl SortDelegate for DatabaseViewSortDelegateImpl {
fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut<Option<Arc<Sort>>> {
let sort = self.delegate.get_sort(view_id, sort_id).map(Arc::new);
to_fut(async move { sort })
}
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>> {
let view_id = view_id.to_string();
let delegate = self.delegate.clone();
let filter_controller = self.filter_controller.clone();
to_fut(async move {
let mut rows = delegate.get_rows(&view_id).await;
filter_controller.filter_rows(&mut rows).await;
rows
})
}
fn get_field(&self, field_id: &str) -> Fut<Option<Arc<Field>>> {
self.delegate.get_field(field_id)
}
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>> {
self.delegate.get_fields(view_id, field_ids)
}
}

View File

@ -0,0 +1,151 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::rows::{Row, RowId};
use nanoid::nanoid;
use tokio::sync::{broadcast, RwLock};
use flowy_error::FlowyResult;
use lib_infra::future::Fut;
use crate::services::cell::CellCache;
use crate::services::database::{DatabaseRowEvent, MutexDatabase};
use crate::services::database_view::{DatabaseViewData, DatabaseViewEditor};
use crate::services::group::RowChangeset;
pub type RowEventSender = broadcast::Sender<DatabaseRowEvent>;
pub type RowEventReceiver = broadcast::Receiver<DatabaseRowEvent>;
pub struct DatabaseViews {
#[allow(dead_code)]
database: MutexDatabase,
cell_cache: CellCache,
database_view_data: Arc<dyn DatabaseViewData>,
editor_map: Arc<RwLock<HashMap<String, Arc<DatabaseViewEditor>>>>,
}
impl DatabaseViews {
pub async fn new(
database: MutexDatabase,
cell_cache: CellCache,
database_view_data: Arc<dyn DatabaseViewData>,
row_event_rx: RowEventReceiver,
) -> FlowyResult<Self> {
let editor_map = Arc::new(RwLock::new(HashMap::default()));
listen_on_database_row_event(row_event_rx, editor_map.clone());
Ok(Self {
database,
database_view_data,
cell_cache,
editor_map,
})
}
pub async fn close_view(&self, view_id: &str) -> bool {
let mut editor_map = self.editor_map.write().await;
if let Some(view) = editor_map.remove(view_id) {
view.close().await;
}
editor_map.is_empty()
}
pub async fn editors(&self) -> Vec<Arc<DatabaseViewEditor>> {
self.editor_map.read().await.values().cloned().collect()
}
/// It may generate a RowChangeset when the Row was moved from one group to another.
/// The return value, [RowChangeset], contains the changes made by the groups.
///
pub async fn move_group_row(
&self,
view_id: &str,
row: Arc<Row>,
to_group_id: String,
to_row_id: Option<RowId>,
recv_row_changeset: impl FnOnce(RowChangeset) -> Fut<()>,
) -> FlowyResult<()> {
let view_editor = self.get_view_editor(view_id).await?;
let mut row_changeset = RowChangeset::new(row.id);
view_editor
.v_move_group_row(&row, &mut row_changeset, &to_group_id, to_row_id)
.await;
if !row_changeset.is_empty() {
recv_row_changeset(row_changeset).await;
}
Ok(())
}
/// Notifies the view's field type-option data is changed
/// For the moment, only the groups will be generated after the type-option data changed. A
/// [Field] has a property named type_options contains a list of type-option data.
/// # Arguments
///
/// * `field_id`: the id of the field in current view
///
#[tracing::instrument(level = "debug", skip(self, old_field), err)]
pub async fn did_update_field_type_option(
&self,
view_id: &str,
field_id: &str,
old_field: &Field,
) -> FlowyResult<()> {
let view_editor = self.get_view_editor(view_id).await?;
// If the id of the grouping field is equal to the updated field's id, then we need to
// update the group setting
if view_editor.group_id().await == field_id {
view_editor.v_update_group_setting(field_id).await?;
}
view_editor
.v_did_update_field_type_option(field_id, old_field)
.await?;
Ok(())
}
pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<DatabaseViewEditor>> {
debug_assert!(!view_id.is_empty());
if let Some(editor) = self.editor_map.read().await.get(view_id) {
return Ok(editor.clone());
}
tracing::trace!("{:p} create view:{} editor", self, view_id);
let mut editor_map = self.editor_map.write().await;
let editor = Arc::new(
DatabaseViewEditor::new(
view_id.to_owned(),
self.database_view_data.clone(),
self.cell_cache.clone(),
)
.await?,
);
editor_map.insert(view_id.to_owned(), editor.clone());
Ok(editor)
}
}
fn listen_on_database_row_event(
mut row_event_rx: broadcast::Receiver<DatabaseRowEvent>,
view_editors: Arc<RwLock<HashMap<String, Arc<DatabaseViewEditor>>>>,
) {
tokio::spawn(async move {
while let Ok(event) = row_event_rx.recv().await {
let read_guard = view_editors.read().await;
let view_editors = read_guard.values();
let event = if view_editors.len() == 1 {
Cow::Owned(event)
} else {
Cow::Borrowed(&event)
};
for view_editor in view_editors {
view_editor.handle_block_event(event.clone()).await;
}
}
});
}
pub fn gen_handler_id() -> String {
nanoid!(10)
}

View File

@ -0,0 +1,53 @@
use crate::entities::FieldType;
use crate::services::field::default_type_option_data_from_type;
use collab_database::database::gen_field_id;
use collab_database::fields::{Field, TypeOptionData};
pub struct FieldBuilder {
field: Field,
}
impl FieldBuilder {
pub fn new<T: Into<TypeOptionData>>(field_type: FieldType, type_option_data: T) -> Self {
let mut field = Field::new(
gen_field_id(),
"".to_string(),
field_type.clone().into(),
false,
);
field.width = field_type.default_cell_width() as i64;
field
.type_options
.insert(field_type.to_string(), type_option_data.into());
Self { field }
}
pub fn from_field_type(field_type: FieldType) -> Self {
let type_option_data = default_type_option_data_from_type(&field_type);
Self::new(field_type, type_option_data)
}
pub fn name(mut self, name: &str) -> Self {
self.field.name = name.to_owned();
self
}
pub fn primary(mut self, is_primary: bool) -> Self {
self.field.is_primary = is_primary;
self
}
pub fn visibility(mut self, visibility: bool) -> Self {
self.field.visibility = visibility;
self
}
pub fn width(mut self, width: i64) -> Self {
self.field.width = width;
self
}
pub fn build(self) -> Field {
self.field
}
}

View File

@ -0,0 +1,52 @@
use std::sync::Arc;
use collab_database::fields::TypeOptionData;
use flowy_error::FlowyResult;
use crate::entities::FieldType;
use crate::services::database::DatabaseEditor;
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
pub async fn edit_field_type_option<T: From<TypeOptionData> + Into<TypeOptionData>>(
view_id: &str,
field_id: &str,
editor: Arc<DatabaseEditor>,
action: impl FnOnce(&mut T),
) -> FlowyResult<()> {
let get_type_option = async {
let field = editor.get_field(field_id)?;
let field_type = FieldType::from(field.field_type);
field.get_type_option::<T>(field_type)
};
if let Some(mut type_option) = get_type_option.await {
if let Some(old_field) = editor.get_field(field_id) {
action(&mut type_option);
let type_option_data: TypeOptionData = type_option.into();
editor
.update_field_type_option(view_id, field_id, type_option_data, old_field)
.await?;
}
}
Ok(())
}
pub async fn edit_single_select_type_option(
view_id: &str,
field_id: &str,
editor: Arc<DatabaseEditor>,
action: impl FnOnce(&mut SingleSelectTypeOption),
) -> FlowyResult<()> {
edit_field_type_option(view_id, field_id, editor, action).await
}
pub async fn edit_multi_select_type_option(
view_id: &str,
field_id: &str,
editor: Arc<DatabaseEditor>,
action: impl FnOnce(&mut MultiSelectTypeOption),
) -> FlowyResult<()> {
edit_field_type_option(view_id, field_id, editor, action).await
}

View File

@ -0,0 +1,9 @@
mod field_builder;
mod field_operation;
mod type_option_builder;
mod type_options;
pub use field_builder::*;
pub use field_operation::*;
pub use type_option_builder::*;
pub use type_options::*;

View File

@ -0,0 +1,16 @@
use crate::entities::FieldType;
use crate::services::field::type_options::*;
use collab_database::fields::TypeOptionData;
pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionData {
match field_type {
FieldType::RichText => RichTextTypeOption::default().into(),
FieldType::Number => NumberTypeOption::default().into(),
FieldType::DateTime => DateTypeOption::default().into(),
FieldType::SingleSelect => SingleSelectTypeOption::default().into(),
FieldType::MultiSelect => MultiSelectTypeOption::default().into(),
FieldType::Checkbox => CheckboxTypeOption::default().into(),
FieldType::URL => URLTypeOption::default().into(),
FieldType::Checklist => ChecklistTypeOption::default().into(),
}
}

View File

@ -0,0 +1,51 @@
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};
use crate::services::field::CheckboxCellData;
impl CheckboxFilterPB {
pub fn is_visible(&self, cell_data: &CheckboxCellData) -> bool {
let is_check = cell_data.is_check();
match self.condition {
CheckboxFilterConditionPB::IsChecked => is_check,
CheckboxFilterConditionPB::IsUnChecked => !is_check,
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB};
use crate::services::field::CheckboxCellData;
use std::str::FromStr;
#[test]
fn checkbox_filter_is_check_test() {
let checkbox_filter = CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsChecked,
};
for (value, visible) in [
("true", true),
("yes", true),
("false", false),
("no", false),
] {
let data = CheckboxCellData::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}
#[test]
fn checkbox_filter_is_uncheck_test() {
let checkbox_filter = CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsUnChecked,
};
for (value, visible) in [
("false", true),
("no", true),
("true", false),
("yes", false),
] {
let data = CheckboxCellData::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}
}

View File

@ -0,0 +1,54 @@
#[cfg(test)]
mod tests {
use collab_database::fields::Field;
use crate::entities::FieldType;
use crate::services::cell::CellDataDecoder;
use crate::services::cell::FromCellString;
use crate::services::field::type_options::checkbox_type_option::*;
use crate::services::field::FieldBuilder;
#[test]
fn checkout_box_description_test() {
let type_option = CheckboxTypeOption::default();
let field_type = FieldType::Checkbox;
let field_rev = FieldBuilder::from_field_type(field_type.clone()).build();
// the checkout value will be checked if the value is "1", "true" or "yes"
assert_checkbox(&type_option, "1", CHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "true", CHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "TRUE", CHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "yes", CHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "YES", CHECK, &field_type, &field_rev);
// the checkout value will be uncheck if the value is "false" or "No"
assert_checkbox(&type_option, "false", UNCHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "No", UNCHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "NO", UNCHECK, &field_type, &field_rev);
assert_checkbox(&type_option, "0", UNCHECK, &field_type, &field_rev);
// the checkout value will be empty if the value is letters or empty string
assert_checkbox(&type_option, "abc", "", &field_type, &field_rev);
assert_checkbox(&type_option, "", "", &field_type, &field_rev);
}
fn assert_checkbox(
type_option: &CheckboxTypeOption,
input_str: &str,
expected_str: &str,
field_type: &FieldType,
field: &Field,
) {
assert_eq!(
type_option
.decode_cell_str(
&CheckboxCellData::from_cell_str(input_str).unwrap().into(),
field_type,
field
)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,145 @@
use crate::entities::{CheckboxFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, CheckboxCellData, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform,
};
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CheckboxTypeOption {
pub is_selected: bool,
}
impl TypeOption for CheckboxTypeOption {
type CellData = CheckboxCellData;
type CellChangeset = CheckboxCellChangeset;
type CellProtobufType = CheckboxCellData;
type CellFilter = CheckboxFilterPB;
}
impl TypeOptionTransform for CheckboxTypeOption {
fn transformable(&self) -> bool {
true
}
fn transform_type_option(
&mut self,
_old_type_option_field_type: FieldType,
_old_type_option_data: TypeOptionData,
) {
}
fn transform_type_option_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
if _decoded_field_type.is_text() {
Some(CheckboxCellData::from(cell))
} else {
None
}
}
}
impl From<TypeOptionData> for CheckboxTypeOption {
fn from(data: TypeOptionData) -> Self {
let is_selected = data.get_bool_value("is_selected").unwrap_or(false);
CheckboxTypeOption { is_selected }
}
}
impl From<CheckboxTypeOption> for TypeOptionData {
fn from(data: CheckboxTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_bool_value("is_selected", data.is_selected)
.build()
}
}
impl TypeOptionCellData for CheckboxTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
cell_data
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(CheckboxCellData::from(cell))
}
}
impl CellDataDecoder for CheckboxTypeOption {
fn decode_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_checkbox() {
return Ok(Default::default());
}
self.decode_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string()
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
}
pub type CheckboxCellChangeset = String;
impl CellDataChangeset for CheckboxTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let checkbox_cell_data = CheckboxCellData::from_str(&changeset)?;
Ok((checkbox_cell_data.clone().into(), checkbox_cell_data))
}
}
impl TypeOptionCellDataFilter for CheckboxTypeOption {
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 CheckboxTypeOption {
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

@ -0,0 +1,115 @@
use crate::entities::FieldType;
use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString};
use crate::services::field::CELL_DATE;
use bytes::Bytes;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::{FlowyError, FlowyResult};
use protobuf::ProtobufError;
use std::str::FromStr;
pub const CHECK: &str = "Yes";
pub const UNCHECK: &str = "No";
#[derive(Default, Debug, Clone)]
pub struct CheckboxCellData(pub String);
impl CheckboxCellData {
pub fn into_inner(self) -> bool {
self.is_check()
}
pub fn is_check(&self) -> bool {
self.0 == CHECK
}
pub fn is_uncheck(&self) -> bool {
self.0 == UNCHECK
}
}
impl AsRef<[u8]> for CheckboxCellData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl From<&Cell> for CheckboxCellData {
fn from(cell: &Cell) -> Self {
let value = cell.get_str_value(CELL_DATE).unwrap_or_default();
CheckboxCellData::from_cell_str(&value).unwrap_or_default()
}
}
impl From<CheckboxCellData> for Cell {
fn from(data: CheckboxCellData) -> Self {
new_cell_builder(FieldType::Checkbox)
.insert_str_value(CELL_DATE, data.to_string())
.build()
}
}
impl FromStr for CheckboxCellData {
type Err = FlowyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower_case_str: &str = &s.to_lowercase();
let val = match lower_case_str {
"1" => Some(true),
"true" => Some(true),
"yes" => Some(true),
"0" => Some(false),
"false" => Some(false),
"no" => Some(false),
_ => None,
};
match val {
Some(true) => Ok(Self(CHECK.to_string())),
Some(false) => Ok(Self(UNCHECK.to_string())),
None => Ok(Self("".to_string())),
}
}
}
impl std::convert::TryFrom<CheckboxCellData> for Bytes {
type Error = ProtobufError;
fn try_from(value: CheckboxCellData) -> Result<Self, Self::Error> {
Ok(Bytes::from(value.0))
}
}
impl FromCellString for CheckboxCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Self::from_str(s)
}
}
impl ToString for CheckboxCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl DecodedCellData for CheckboxCellData {
type Object = CheckboxCellData;
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub struct CheckboxCellDataParser();
impl CellProtobufBlobParser for CheckboxCellDataParser {
type Object = CheckboxCellData;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => CheckboxCellData::from_cell_str(&s),
Err(_) => Ok(CheckboxCellData("".to_string())),
}
}
}

View File

@ -0,0 +1,8 @@
#![allow(clippy::module_inception)]
mod checkbox_filter;
mod checkbox_tests;
mod checkbox_type_option;
mod checkbox_type_option_entities;
pub use checkbox_type_option::*;
pub use checkbox_type_option_entities::*;

View File

@ -0,0 +1,149 @@
use crate::entities::{DateFilterConditionPB, DateFilterPB};
use chrono::NaiveDateTime;
impl DateFilterPB {
pub fn is_visible<T: Into<Option<i64>>>(&self, cell_timestamp: T) -> bool {
match cell_timestamp.into() {
None => DateFilterConditionPB::DateIsEmpty == self.condition,
Some(timestamp) => {
match self.condition {
DateFilterConditionPB::DateIsNotEmpty => {
return true;
},
DateFilterConditionPB::DateIsEmpty => {
return false;
},
_ => {},
}
let cell_time = NaiveDateTime::from_timestamp_opt(timestamp, 0);
let cell_date = cell_time.map(|time| time.date());
match self.timestamp {
None => {
if self.start.is_none() {
return true;
}
if self.end.is_none() {
return true;
}
let start_time = NaiveDateTime::from_timestamp_opt(*self.start.as_ref().unwrap(), 0);
let start_date = start_time.map(|time| time.date());
let end_time = NaiveDateTime::from_timestamp_opt(*self.end.as_ref().unwrap(), 0);
let end_date = end_time.map(|time| time.date());
cell_date >= start_date && cell_date <= end_date
},
Some(timestamp) => {
let expected_timestamp = NaiveDateTime::from_timestamp_opt(timestamp, 0);
let expected_date = expected_timestamp.map(|time| time.date());
// We assume that the cell_timestamp doesn't contain hours, just day.
match self.condition {
DateFilterConditionPB::DateIs => cell_date == expected_date,
DateFilterConditionPB::DateBefore => cell_date < expected_date,
DateFilterConditionPB::DateAfter => cell_date > expected_date,
DateFilterConditionPB::DateOnOrBefore => cell_date <= expected_date,
DateFilterConditionPB::DateOnOrAfter => cell_date >= expected_date,
_ => true,
}
},
}
},
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]
use crate::entities::{DateFilterConditionPB, DateFilterPB};
#[test]
fn date_filter_is_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateIs,
timestamp: Some(1668387885),
end: None,
start: None,
};
for (val, visible) in vec![(1668387885, true), (1647251762, false)] {
assert_eq!(filter.is_visible(val as i64), visible);
}
}
#[test]
fn date_filter_before_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateBefore,
timestamp: Some(1668387885),
start: None,
end: None,
};
for (val, visible, msg) in vec![(1668387884, false, "1"), (1647251762, true, "2")] {
assert_eq!(filter.is_visible(val as i64), visible, "{}", msg);
}
}
#[test]
fn date_filter_before_or_on_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateOnOrBefore,
timestamp: Some(1668387885),
start: None,
end: None,
};
for (val, visible) in vec![(1668387884, true), (1668387885, true)] {
assert_eq!(filter.is_visible(val as i64), visible);
}
}
#[test]
fn date_filter_after_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateAfter,
timestamp: Some(1668387885),
start: None,
end: None,
};
for (val, visible) in vec![(1668387888, false), (1668531885, true), (0, false)] {
assert_eq!(filter.is_visible(val as i64), visible);
}
}
#[test]
fn date_filter_within_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateWithIn,
start: Some(1668272685), // 11/13
end: Some(1668618285), // 11/17
timestamp: None,
};
for (val, visible, _msg) in vec![
(1668272685, true, "11/13"),
(1668359085, true, "11/14"),
(1668704685, false, "11/18"),
] {
assert_eq!(filter.is_visible(val as i64), visible);
}
}
#[test]
fn date_filter_is_empty_test() {
let filter = DateFilterPB {
condition: DateFilterConditionPB::DateIsEmpty,
start: None,
end: None,
timestamp: None,
};
for (val, visible) in vec![(None, true), (Some(123), false)] {
assert_eq!(filter.is_visible(val), visible);
}
}
}

View File

@ -0,0 +1,261 @@
#[cfg(test)]
mod tests {
use chrono::format::strftime::StrftimeItems;
use chrono::{FixedOffset, NaiveDateTime};
use collab_database::fields::Field;
use collab_database::rows::Cell;
use strum::IntoEnumIterator;
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat, TypeOptionCellData,
};
#[test]
fn date_type_option_date_format_test() {
let mut type_option = DateTypeOption::default();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
for date_format in DateFormat::iter() {
type_option.date_format = date_format;
match date_format {
DateFormat::Friendly => {
assert_date(&type_option, 1647251762, None, "Mar 14,2022", false, &field);
},
DateFormat::US => {
assert_date(&type_option, 1647251762, None, "2022/03/14", false, &field);
},
DateFormat::ISO => {
assert_date(&type_option, 1647251762, None, "2022-03-14", false, &field);
},
DateFormat::Local => {
assert_date(&type_option, 1647251762, None, "03/14/2022", false, &field);
},
DateFormat::DayMonthYear => {
assert_date(&type_option, 1647251762, None, "14/03/2022", false, &field);
},
}
}
}
#[test]
fn date_type_option_different_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
for time_format in TimeFormat::iter() {
type_option.time_format = time_format;
match time_format {
TimeFormat::TwentyFourHour => {
assert_date(
&type_option,
1653609600,
None,
"May 27,2022 00:00",
true,
&field_rev,
);
assert_date(
&type_option,
1653609600,
Some("9:00".to_owned()),
"May 27,2022 09:00",
true,
&field_rev,
);
assert_date(
&type_option,
1653609600,
Some("23:00".to_owned()),
"May 27,2022 23:00",
true,
&field_rev,
);
},
TimeFormat::TwelveHour => {
assert_date(
&type_option,
1653609600,
None,
"May 27,2022 12:00 AM",
true,
&field_rev,
);
assert_date(
&type_option,
1653609600,
Some("9:00 AM".to_owned()),
"May 27,2022 09:00 AM",
true,
&field_rev,
);
assert_date(
&type_option,
1653609600,
Some("11:23 pm".to_owned()),
"May 27,2022 11:23 PM",
true,
&field_rev,
);
},
}
}
}
#[test]
fn date_type_option_invalid_date_str_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
assert_date(&type_option, "abc", None, "", false, &field_rev);
}
#[test]
#[should_panic]
fn date_type_option_invalid_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("1:".to_owned()),
"May 27,2022 01:00",
true,
&field_rev,
);
}
#[test]
fn date_type_option_empty_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("".to_owned()),
"May 27,2022 00:00",
true,
&field_rev,
);
}
#[test]
fn date_type_midnight_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
assert_date(
&type_option,
1653609600,
Some("00:00".to_owned()),
"May 27,2022 00:00",
true,
&field_rev,
);
}
/// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error.
#[test]
#[should_panic]
fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("1:00 am".to_owned()),
"May 27,2022 01:00 AM",
true,
&field_rev,
);
}
// Attempting to parse include_time_str as TwelveHour when TwentyFourHour format is given should cause parser error.
#[test]
#[should_panic]
fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
let mut type_option = DateTypeOption::new();
type_option.time_format = TimeFormat::TwelveHour;
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("20:00".to_owned()),
"May 27,2022 08:00 PM",
true,
&field_rev,
);
}
#[test]
fn utc_to_native_test() {
let native_timestamp = 1647251762;
let native = NaiveDateTime::from_timestamp_opt(native_timestamp, 0).unwrap();
let utc = chrono::DateTime::<chrono::Utc>::from_utc(native, chrono::Utc);
// utc_timestamp doesn't carry timezone
let utc_timestamp = utc.timestamp();
assert_eq!(native_timestamp, utc_timestamp);
let format = "%m/%d/%Y %I:%M %p".to_string();
let native_time_str = format!("{}", native.format_with_items(StrftimeItems::new(&format)));
let utc_time_str = format!("{}", utc.format_with_items(StrftimeItems::new(&format)));
assert_eq!(native_time_str, utc_time_str);
// Mon Mar 14 2022 17:56:02 GMT+0800 (China Standard Time)
let gmt_8_offset = FixedOffset::east_opt(8 * 3600).unwrap();
let china_local = chrono::DateTime::<chrono::Local>::from_utc(native, gmt_8_offset);
let china_local_time = format!(
"{}",
china_local.format_with_items(StrftimeItems::new(&format))
);
assert_eq!(china_local_time, "03/14/2022 05:56 PM");
}
fn assert_date<T: ToString>(
type_option: &DateTypeOption,
timestamp: T,
include_time_str: Option<String>,
expected_str: &str,
include_time: bool,
field: &Field,
) {
let changeset = DateCellChangeset {
date: Some(timestamp.to_string()),
time: include_time_str,
is_utc: false,
include_time: Some(include_time),
};
let (cell, _) = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
decode_cell_data(&cell, type_option, include_time, field),
expected_str.to_owned(),
);
}
fn decode_cell_data(
cell: &Cell,
type_option: &DateTypeOption,
include_time: bool,
field: &Field,
) -> String {
let decoded_data = type_option
.decode_cell_str(cell, &FieldType::DateTime, field)
.unwrap();
let decoded_data = type_option.convert_to_protobuf(decoded_data);
if include_time {
format!("{} {}", decoded_data.date, decoded_data.time)
.trim_end()
.to_owned()
} else {
decoded_data.date
}
}
}

View File

@ -0,0 +1,234 @@
use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption,
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
};
use chrono::format::strftime::StrftimeItems;
use chrono::NaiveDateTime;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
// Date
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DateTypeOption {
pub date_format: DateFormat,
pub time_format: TimeFormat,
pub include_time: bool,
}
impl TypeOption for DateTypeOption {
type CellData = DateCellData;
type CellChangeset = DateCellChangeset;
type CellProtobufType = DateCellDataPB;
type CellFilter = DateFilterPB;
}
impl From<TypeOptionData> for DateTypeOption {
fn from(data: TypeOptionData) -> Self {
let include_time = data.get_bool_value("include_time").unwrap_or(false);
let date_format = data
.get_i64_value("data_format")
.map(DateFormat::from)
.unwrap_or_default();
let time_format = data
.get_i64_value("time_format")
.map(TimeFormat::from)
.unwrap_or_default();
Self {
date_format,
time_format,
include_time,
}
}
}
impl From<DateTypeOption> for TypeOptionData {
fn from(data: DateTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_i64_value("data_format", data.date_format.value())
.insert_i64_value("time_format", data.time_format.value())
.insert_bool_value("include_time", data.include_time)
.build()
}
}
impl TypeOptionCellData for DateTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.today_desc_from_timestamp(cell_data)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(DateCellData::from(cell))
}
}
impl DateTypeOption {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
let timestamp = cell_data.timestamp.unwrap_or_default();
let include_time = cell_data.include_time;
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
if naive.is_none() {
return DateCellDataPB::default();
}
let naive = naive.unwrap();
if timestamp == 0 {
return DateCellDataPB::default();
}
let fmt = self.date_format.format_str();
let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt)));
let time = if include_time {
let fmt = self.time_format.format_str();
format!("{}", naive.format_with_items(StrftimeItems::new(fmt)))
} else {
"".to_string()
};
DateCellDataPB {
date,
time,
include_time,
timestamp,
}
}
fn timestamp_from_utc_with_time(
&self,
naive_date: &NaiveDateTime,
time_str: &Option<String>,
) -> FlowyResult<i64> {
if let Some(time_str) = time_str.as_ref() {
if !time_str.is_empty() {
let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str());
match naive_time {
Ok(naive_time) => {
return Ok(naive_date.date().and_time(naive_time).timestamp());
},
Err(_e) => {
let msg = format!("Parse {} failed", time_str);
return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg));
},
};
}
}
Ok(naive_date.timestamp())
}
}
impl TypeOptionTransform for DateTypeOption {}
impl CellDataDecoder for DateTypeOption {
fn decode_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(Default::default());
}
self.decode_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
self.today_desc_from_timestamp(cell_data).date
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
}
}
impl CellDataChangeset for DateTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let (timestamp, include_time) = match cell {
None => (None, false),
Some(cell) => {
let cell_data = DateCellData::from(&cell);
(cell_data.timestamp, cell_data.include_time)
},
};
let include_time = match changeset.include_time {
None => include_time,
Some(include_time) => include_time,
};
let timestamp = match changeset.date_timestamp() {
None => timestamp,
Some(date_timestamp) => match (include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
if let Some(naive) = naive {
Some(self.timestamp_from_utc_with_time(&naive, &time)?)
} else {
Some(date_timestamp)
}
},
_ => Some(date_timestamp),
},
};
let date_cell_data = DateCellData {
timestamp,
include_time,
};
Ok((date_cell_data.clone().into(), date_cell_data))
}
}
impl TypeOptionCellDataFilter for DateTypeOption {
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.timestamp)
}
}
impl TypeOptionCellDataCompare for DateTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
match (cell_data.timestamp, other_cell_data.timestamp) {
(Some(left), Some(right)) => left.cmp(&right),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => default_order(),
}
}
}

View File

@ -0,0 +1,266 @@
#![allow(clippy::upper_case_acronyms)]
use std::fmt;
use bytes::Bytes;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use serde::de::Visitor;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use flowy_error::{internal_error, FlowyResult};
use crate::entities::{DateCellDataPB, FieldType};
use crate::services::cell::{
CellProtobufBlobParser, DecodedCellData, FromCellChangeset, FromCellString, ToCellChangeset,
};
use crate::services::field::CELL_DATE;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DateCellChangeset {
pub date: Option<String>,
pub time: Option<String>,
pub include_time: Option<bool>,
pub is_utc: bool,
}
impl DateCellChangeset {
pub fn date_timestamp(&self) -> Option<i64> {
if let Some(date) = &self.date {
match date.parse::<i64>() {
Ok(date_timestamp) => Some(date_timestamp),
Err(_) => None,
}
} else {
None
}
}
}
impl FromCellChangeset for DateCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<DateCellChangeset>(&changeset).map_err(internal_error)
}
}
impl ToCellChangeset for DateCellChangeset {
fn to_cell_changeset_str(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
}
#[derive(Default, Clone, Debug, Serialize)]
pub struct DateCellData {
pub timestamp: Option<i64>,
pub include_time: bool,
}
impl From<&Cell> for DateCellData {
fn from(cell: &Cell) -> Self {
let timestamp = cell
.get_str_value(CELL_DATE)
.map(|data| data.parse::<i64>().unwrap_or_default());
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
Self {
timestamp,
include_time,
}
}
}
impl From<DateCellData> for Cell {
fn from(data: DateCellData) -> Self {
new_cell_builder(FieldType::DateTime)
.insert_str_value(CELL_DATE, data.timestamp.unwrap_or_default().to_string())
.insert_bool_value("include_time", data.include_time)
.build()
}
}
impl<'de> serde::Deserialize<'de> for DateCellData {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct DateCellVisitor();
impl<'de> Visitor<'de> for DateCellVisitor {
type Value = DateCellData;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"DateCellData with type: str containing either an integer timestamp or the JSON representation",
)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(DateCellData {
timestamp: Some(value),
include_time: false,
})
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_i64(value as i64)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut timestamp: Option<i64> = None;
let mut include_time: Option<bool> = None;
while let Some(key) = map.next_key()? {
match key {
"timestamp" => {
timestamp = map.next_value()?;
},
"include_time" => {
include_time = map.next_value()?;
},
_ => {},
}
}
let include_time = include_time.unwrap_or(false);
Ok(DateCellData {
timestamp,
include_time,
})
}
}
deserializer.deserialize_any(DateCellVisitor())
}
}
impl FromCellString for DateCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
let result: DateCellData = serde_json::from_str(s).unwrap();
Ok(result)
}
}
impl ToString for DateCellData {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize)]
pub enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
DayMonthYear = 4,
}
impl std::default::Default for DateFormat {
fn default() -> Self {
DateFormat::Friendly
}
}
impl std::convert::From<i64> for DateFormat {
fn from(value: i64) -> Self {
match value {
0 => DateFormat::Local,
1 => DateFormat::US,
2 => DateFormat::ISO,
3 => DateFormat::Friendly,
4 => DateFormat::DayMonthYear,
_ => {
tracing::error!("Unsupported date format, fallback to friendly");
DateFormat::Friendly
},
}
}
}
impl DateFormat {
pub fn value(&self) -> i64 {
*self as i64
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
DateFormat::Local => "%m/%d/%Y",
DateFormat::US => "%Y/%m/%d",
DateFormat::ISO => "%Y-%m-%d",
DateFormat::Friendly => "%b %d,%Y",
DateFormat::DayMonthYear => "%d/%m/%Y",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize)]
pub enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}
impl std::convert::From<i64> for TimeFormat {
fn from(value: i64) -> Self {
match value {
0 => TimeFormat::TwelveHour,
1 => TimeFormat::TwentyFourHour,
_ => {
tracing::error!("Unsupported time format, fallback to TwentyFourHour");
TimeFormat::TwentyFourHour
},
}
}
}
impl TimeFormat {
pub fn value(&self) -> i64 {
*self as i64
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
TimeFormat::TwelveHour => "%I:%M %p",
TimeFormat::TwentyFourHour => "%R",
}
}
}
impl std::default::Default for TimeFormat {
fn default() -> Self {
TimeFormat::TwentyFourHour
}
}
impl DecodedCellData for DateCellDataPB {
type Object = DateCellDataPB;
fn is_empty(&self) -> bool {
self.date.is_empty()
}
}
pub struct DateCellDataParser();
impl CellProtobufBlobParser for DateCellDataParser {
type Object = DateCellDataPB;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error)
}
}

View File

@ -0,0 +1,8 @@
#![allow(clippy::module_inception)]
mod date_filter;
mod date_tests;
mod date_type_option;
mod date_type_option_entities;
pub use date_type_option::*;
pub use date_type_option_entities::*;

View File

@ -0,0 +1,18 @@
pub mod checkbox_type_option;
pub mod date_type_option;
pub mod number_type_option;
pub mod selection_type_option;
pub mod text_type_option;
mod type_option;
mod type_option_cell;
mod url_type_option;
mod util;
pub use checkbox_type_option::*;
pub use date_type_option::*;
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

@ -0,0 +1,504 @@
#![allow(clippy::upper_case_acronyms)]
use lazy_static::lazy_static;
use rusty_money::define_currency_set;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
lazy_static! {
pub static ref CURRENCY_SYMBOL: Vec<String> = NumberFormat::iter()
.map(|format| format.symbol())
.collect::<Vec<String>>();
pub static ref STRIP_SYMBOL: Vec<String> = vec![",".to_owned(), ".".to_owned()];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize)]
pub enum NumberFormat {
Num = 0,
USD = 1,
CanadianDollar = 2,
EUR = 4,
Pound = 5,
Yen = 6,
Ruble = 7,
Rupee = 8,
Won = 9,
Yuan = 10,
Real = 11,
Lira = 12,
Rupiah = 13,
Franc = 14,
HongKongDollar = 15,
NewZealandDollar = 16,
Krona = 17,
NorwegianKrone = 18,
MexicanPeso = 19,
Rand = 20,
NewTaiwanDollar = 21,
DanishKrone = 22,
Baht = 23,
Forint = 24,
Koruna = 25,
Shekel = 26,
ChileanPeso = 27,
PhilippinePeso = 28,
Dirham = 29,
ColombianPeso = 30,
Riyal = 31,
Ringgit = 32,
Leu = 33,
ArgentinePeso = 34,
UruguayanPeso = 35,
Percent = 36,
}
impl NumberFormat {
pub fn value(&self) -> i64 {
*self as i64
}
}
impl std::default::Default for NumberFormat {
fn default() -> Self {
NumberFormat::Num
}
}
impl From<i64> for NumberFormat {
fn from(value: i64) -> Self {
match value {
0 => NumberFormat::Num,
1 => NumberFormat::USD,
2 => NumberFormat::CanadianDollar,
4 => NumberFormat::EUR,
5 => NumberFormat::Pound,
6 => NumberFormat::Yen,
7 => NumberFormat::Ruble,
8 => NumberFormat::Rupee,
9 => NumberFormat::Won,
10 => NumberFormat::Yuan,
11 => NumberFormat::Real,
12 => NumberFormat::Lira,
13 => NumberFormat::Rupiah,
14 => NumberFormat::Franc,
15 => NumberFormat::HongKongDollar,
16 => NumberFormat::NewZealandDollar,
17 => NumberFormat::Krona,
18 => NumberFormat::NorwegianKrone,
19 => NumberFormat::MexicanPeso,
20 => NumberFormat::Rand,
21 => NumberFormat::NewTaiwanDollar,
22 => NumberFormat::DanishKrone,
23 => NumberFormat::Baht,
24 => NumberFormat::Forint,
25 => NumberFormat::Koruna,
26 => NumberFormat::Shekel,
27 => NumberFormat::ChileanPeso,
28 => NumberFormat::PhilippinePeso,
29 => NumberFormat::Dirham,
30 => NumberFormat::ColombianPeso,
31 => NumberFormat::Riyal,
32 => NumberFormat::Ringgit,
33 => NumberFormat::Leu,
34 => NumberFormat::ArgentinePeso,
35 => NumberFormat::UruguayanPeso,
36 => NumberFormat::Percent,
_ => NumberFormat::Num,
}
}
}
define_currency_set!(
number_currency {
NUMBER : {
code: "",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "number",
symbol: "RUB",
symbol_first: false,
},
PERCENT : {
code: "",
exponent: 2,
locale: EnIn,
minor_units: 1,
name: "percent",
symbol: "%",
symbol_first: false,
},
USD : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "United States Dollar",
symbol: "$",
symbol_first: true,
},
CANADIAN_DOLLAR : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Canadian Dollar",
symbol: "CA$",
symbol_first: true,
},
NEW_TAIWAN_DOLLAR : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "NewTaiwan Dollar",
symbol: "NT$",
symbol_first: true,
},
HONG_KONG_DOLLAR : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "HongKong Dollar",
symbol: "HZ$",
symbol_first: true,
},
NEW_ZEALAND_DOLLAR : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "NewZealand Dollar",
symbol: "NZ$",
symbol_first: true,
},
EUR : {
code: "EUR",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Euro",
symbol: "",
symbol_first: true,
},
GIP : {
code: "GIP",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Gibraltar Pound",
symbol: "£",
symbol_first: true,
},
CNY : {
code: "CNY",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Chinese Renminbi Yuan",
symbol: "¥",
symbol_first: true,
},
YUAN : {
code: "CNY",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Chinese Renminbi Yuan",
symbol: "CN¥",
symbol_first: true,
},
RUB : {
code: "RUB",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Russian Ruble",
symbol: "RUB",
symbol_first: false,
},
INR : {
code: "INR",
exponent: 2,
locale: EnIn,
minor_units: 50,
name: "Indian Rupee",
symbol: "",
symbol_first: true,
},
KRW : {
code: "KRW",
exponent: 0,
locale: EnUs,
minor_units: 1,
name: "South Korean Won",
symbol: "",
symbol_first: true,
},
BRL : {
code: "BRL",
exponent: 2,
locale: EnUs,
minor_units: 5,
name: "Brazilian real",
symbol: "R$",
symbol_first: true,
},
TRY : {
code: "TRY",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Turkish Lira",
// symbol: "₺",
symbol: "TRY",
symbol_first: true,
},
IDR : {
code: "IDR",
exponent: 2,
locale: EnUs,
minor_units: 5000,
name: "Indonesian Rupiah",
// symbol: "Rp",
symbol: "IDR",
symbol_first: true,
},
CHF : {
code: "CHF",
exponent: 2,
locale: EnUs,
minor_units: 5,
name: "Swiss Franc",
// symbol: "Fr",
symbol: "CHF",
symbol_first: true,
},
SEK : {
code: "SEK",
exponent: 2,
locale: EnBy,
minor_units: 100,
name: "Swedish Krona",
// symbol: "kr",
symbol: "SEK",
symbol_first: false,
},
NOK : {
code: "NOK",
exponent: 2,
locale: EnUs,
minor_units: 100,
name: "Norwegian Krone",
// symbol: "kr",
symbol: "NOK",
symbol_first: false,
},
MEXICAN_PESO : {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Mexican Peso",
symbol: "MX$",
symbol_first: true,
},
ZAR : {
code: "ZAR",
exponent: 2,
locale: EnUs,
minor_units: 10,
name: "South African Rand",
// symbol: "R",
symbol: "ZAR",
symbol_first: true,
},
DKK : {
code: "DKK",
exponent: 2,
locale: EnEu,
minor_units: 50,
name: "Danish Krone",
// symbol: "kr.",
symbol: "DKK",
symbol_first: false,
},
THB : {
code: "THB",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Thai Baht",
// symbol: "฿",
symbol: "THB",
symbol_first: true,
},
HUF : {
code: "HUF",
exponent: 0,
locale: EnBy,
minor_units: 5,
name: "Hungarian Forint",
// symbol: "Ft",
symbol: "HUF",
symbol_first: false,
},
KORUNA : {
code: "CZK",
exponent: 2,
locale: EnBy,
minor_units: 100,
name: "Czech Koruna",
// symbol: "Kč",
symbol: "CZK",
symbol_first: false,
},
SHEKEL : {
code: "CZK",
exponent: 2,
locale: EnBy,
minor_units: 100,
name: "Czech Koruna",
symbol: "",
symbol_first: false,
},
CLP : {
code: "CLP",
exponent: 0,
locale: EnEu,
minor_units: 1,
name: "Chilean Peso",
// symbol: "$",
symbol: "CLP",
symbol_first: true,
},
PHP : {
code: "PHP",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "Philippine Peso",
symbol: "",
symbol_first: true,
},
AED : {
code: "AED",
exponent: 2,
locale: EnUs,
minor_units: 25,
name: "United Arab Emirates Dirham",
// symbol: "د.إ",
symbol: "AED",
symbol_first: false,
},
COP : {
code: "COP",
exponent: 2,
locale: EnEu,
minor_units: 20,
name: "Colombian Peso",
// symbol: "$",
symbol: "COP",
symbol_first: true,
},
SAR : {
code: "SAR",
exponent: 2,
locale: EnUs,
minor_units: 5,
name: "Saudi Riyal",
// symbol: "ر.س",
symbol: "SAR",
symbol_first: true,
},
MYR : {
code: "MYR",
exponent: 2,
locale: EnUs,
minor_units: 5,
name: "Malaysian Ringgit",
// symbol: "RM",
symbol: "MYR",
symbol_first: true,
},
RON : {
code: "RON",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Romanian Leu",
// symbol: "ر.ق",
symbol: "RON",
symbol_first: false,
},
ARS : {
code: "ARS",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Argentine Peso",
// symbol: "$",
symbol: "ARS",
symbol_first: true,
},
UYU : {
code: "UYU",
exponent: 2,
locale: EnEu,
minor_units: 100,
name: "Uruguayan Peso",
// symbol: "$U",
symbol: "UYU",
symbol_first: true,
}
}
);
impl NumberFormat {
pub fn currency(&self) -> &'static number_currency::Currency {
match self {
NumberFormat::Num => number_currency::NUMBER,
NumberFormat::USD => number_currency::USD,
NumberFormat::CanadianDollar => number_currency::CANADIAN_DOLLAR,
NumberFormat::EUR => number_currency::EUR,
NumberFormat::Pound => number_currency::GIP,
NumberFormat::Yen => number_currency::CNY,
NumberFormat::Ruble => number_currency::RUB,
NumberFormat::Rupee => number_currency::INR,
NumberFormat::Won => number_currency::KRW,
NumberFormat::Yuan => number_currency::YUAN,
NumberFormat::Real => number_currency::BRL,
NumberFormat::Lira => number_currency::TRY,
NumberFormat::Rupiah => number_currency::IDR,
NumberFormat::Franc => number_currency::CHF,
NumberFormat::HongKongDollar => number_currency::HONG_KONG_DOLLAR,
NumberFormat::NewZealandDollar => number_currency::NEW_ZEALAND_DOLLAR,
NumberFormat::Krona => number_currency::SEK,
NumberFormat::NorwegianKrone => number_currency::NOK,
NumberFormat::MexicanPeso => number_currency::MEXICAN_PESO,
NumberFormat::Rand => number_currency::ZAR,
NumberFormat::NewTaiwanDollar => number_currency::NEW_TAIWAN_DOLLAR,
NumberFormat::DanishKrone => number_currency::DKK,
NumberFormat::Baht => number_currency::THB,
NumberFormat::Forint => number_currency::HUF,
NumberFormat::Koruna => number_currency::KORUNA,
NumberFormat::Shekel => number_currency::SHEKEL,
NumberFormat::ChileanPeso => number_currency::CLP,
NumberFormat::PhilippinePeso => number_currency::PHP,
NumberFormat::Dirham => number_currency::AED,
NumberFormat::ColombianPeso => number_currency::COP,
NumberFormat::Riyal => number_currency::SAR,
NumberFormat::Ringgit => number_currency::MYR,
NumberFormat::Leu => number_currency::RON,
NumberFormat::ArgentinePeso => number_currency::ARS,
NumberFormat::UruguayanPeso => number_currency::UYU,
NumberFormat::Percent => number_currency::PERCENT,
}
}
pub fn symbol(&self) -> String {
self.currency().symbol.to_string()
}
}

View File

@ -0,0 +1,10 @@
#![allow(clippy::module_inception)]
mod format;
mod number_filter;
mod number_tests;
mod number_type_option;
mod number_type_option_entities;
pub use format::*;
pub use number_type_option::*;
pub use number_type_option_entities::*;

View File

@ -0,0 +1,83 @@
use crate::entities::{NumberFilterConditionPB, NumberFilterPB};
use crate::services::field::NumberCellFormat;
use rust_decimal::prelude::Zero;
use rust_decimal::Decimal;
use std::str::FromStr;
impl NumberFilterPB {
pub fn is_visible(&self, num_cell_data: &NumberCellFormat) -> bool {
if self.content.is_empty() {
match self.condition {
NumberFilterConditionPB::NumberIsEmpty => {
return num_cell_data.is_empty();
},
NumberFilterConditionPB::NumberIsNotEmpty => {
return !num_cell_data.is_empty();
},
_ => {},
}
}
match num_cell_data.decimal().as_ref() {
None => false,
Some(cell_decimal) => {
let decimal = Decimal::from_str(&self.content).unwrap_or_else(|_| Decimal::zero());
match self.condition {
NumberFilterConditionPB::Equal => cell_decimal == &decimal,
NumberFilterConditionPB::NotEqual => cell_decimal != &decimal,
NumberFilterConditionPB::GreaterThan => cell_decimal > &decimal,
NumberFilterConditionPB::LessThan => cell_decimal < &decimal,
NumberFilterConditionPB::GreaterThanOrEqualTo => cell_decimal >= &decimal,
NumberFilterConditionPB::LessThanOrEqualTo => cell_decimal <= &decimal,
_ => true,
}
},
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::{NumberFilterConditionPB, NumberFilterPB};
use crate::services::field::{NumberCellFormat, NumberFormat};
#[test]
fn number_filter_equal_test() {
let number_filter = NumberFilterPB {
condition: NumberFilterConditionPB::Equal,
content: "123".to_owned(),
};
for (num_str, visible) in [("123", true), ("1234", false), ("", false)] {
let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
let format = NumberFormat::USD;
for (num_str, visible) in [("$123", true), ("1234", false), ("", false)] {
let data = NumberCellFormat::from_format_str(num_str, true, &format).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
}
#[test]
fn number_filter_greater_than_test() {
let number_filter = NumberFilterPB {
condition: NumberFilterConditionPB::GreaterThan,
content: "12".to_owned(),
};
for (num_str, visible) in [("123", true), ("10", false), ("30", true), ("", false)] {
let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
}
#[test]
fn number_filter_less_than_test() {
let number_filter = NumberFilterPB {
condition: NumberFilterConditionPB::LessThan,
content: "100".to_owned(),
};
for (num_str, visible) in [("12", true), ("1234", false), ("30", true), ("", false)] {
let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
}
}

View File

@ -0,0 +1,672 @@
#[cfg(test)]
mod tests {
use collab_database::fields::Field;
use strum::IntoEnumIterator;
use crate::entities::FieldType;
use crate::services::cell::CellDataDecoder;
use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption};
use crate::services::field::{FieldBuilder, NumberCellData};
/// Testing when the input is not a number.
#[test]
fn number_type_option_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(field_type.clone()).build();
// Input is empty String
assert_number(&type_option, "", "", &field_type, &field_rev);
// Input is letter
assert_number(&type_option, "abc", "", &field_type, &field_rev);
}
/// Testing the strip_currency_symbol function. It should return the string without the input symbol.
#[test]
fn number_type_option_strip_symbol_test() {
// Remove the $ symbol
assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned());
// Remove the ¥ symbol
assert_eq!(strip_currency_symbol("¥0.2"), "0.2".to_owned());
}
/// Format the input number to the corresponding format string.
#[test]
fn number_type_option_format_number_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(field_type.clone()).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_number(&type_option, "18443", "18443", &field_type, &field_rev);
},
NumberFormat::USD => {
assert_number(&type_option, "18443", "$18,443", &field_type, &field_rev);
},
NumberFormat::CanadianDollar => {
assert_number(&type_option, "18443", "CA$18,443", &field_type, &field_rev)
},
NumberFormat::EUR => {
assert_number(&type_option, "18443", "€18.443", &field_type, &field_rev)
},
NumberFormat::Pound => {
assert_number(&type_option, "18443", "£18,443", &field_type, &field_rev)
},
NumberFormat::Yen => {
assert_number(&type_option, "18443", "¥18,443", &field_type, &field_rev);
},
NumberFormat::Ruble => {
assert_number(&type_option, "18443", "18.443RUB", &field_type, &field_rev)
},
NumberFormat::Rupee => {
assert_number(&type_option, "18443", "₹18,443", &field_type, &field_rev)
},
NumberFormat::Won => {
assert_number(&type_option, "18443", "₩18,443", &field_type, &field_rev)
},
NumberFormat::Yuan => {
assert_number(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
},
NumberFormat::Real => {
assert_number(&type_option, "18443", "R$18,443", &field_type, &field_rev);
},
NumberFormat::Lira => {
assert_number(&type_option, "18443", "TRY18.443", &field_type, &field_rev)
},
NumberFormat::Rupiah => {
assert_number(&type_option, "18443", "IDR18,443", &field_type, &field_rev)
},
NumberFormat::Franc => {
assert_number(&type_option, "18443", "CHF18,443", &field_type, &field_rev)
},
NumberFormat::HongKongDollar => {
assert_number(&type_option, "18443", "HZ$18,443", &field_type, &field_rev)
},
NumberFormat::NewZealandDollar => {
assert_number(&type_option, "18443", "NZ$18,443", &field_type, &field_rev)
},
NumberFormat::Krona => {
assert_number(&type_option, "18443", "18 443SEK", &field_type, &field_rev)
},
NumberFormat::NorwegianKrone => {
assert_number(&type_option, "18443", "18,443NOK", &field_type, &field_rev)
},
NumberFormat::MexicanPeso => {
assert_number(&type_option, "18443", "MX$18,443", &field_type, &field_rev)
},
NumberFormat::Rand => {
assert_number(&type_option, "18443", "ZAR18,443", &field_type, &field_rev)
},
NumberFormat::NewTaiwanDollar => {
assert_number(&type_option, "18443", "NT$18,443", &field_type, &field_rev)
},
NumberFormat::DanishKrone => {
assert_number(&type_option, "18443", "18.443DKK", &field_type, &field_rev)
},
NumberFormat::Baht => {
assert_number(&type_option, "18443", "THB18,443", &field_type, &field_rev)
},
NumberFormat::Forint => {
assert_number(&type_option, "18443", "18 443HUF", &field_type, &field_rev)
},
NumberFormat::Koruna => {
assert_number(&type_option, "18443", "18 443CZK", &field_type, &field_rev)
},
NumberFormat::Shekel => {
assert_number(&type_option, "18443", "18 443Kč", &field_type, &field_rev)
},
NumberFormat::ChileanPeso => {
assert_number(&type_option, "18443", "CLP18.443", &field_type, &field_rev)
},
NumberFormat::PhilippinePeso => {
assert_number(&type_option, "18443", "₱18,443", &field_type, &field_rev)
},
NumberFormat::Dirham => {
assert_number(&type_option, "18443", "18,443AED", &field_type, &field_rev)
},
NumberFormat::ColombianPeso => {
assert_number(&type_option, "18443", "COP18.443", &field_type, &field_rev)
},
NumberFormat::Riyal => {
assert_number(&type_option, "18443", "SAR18,443", &field_type, &field_rev)
},
NumberFormat::Ringgit => {
assert_number(&type_option, "18443", "MYR18,443", &field_type, &field_rev)
},
NumberFormat::Leu => {
assert_number(&type_option, "18443", "18.443RON", &field_type, &field_rev)
},
NumberFormat::ArgentinePeso => {
assert_number(&type_option, "18443", "ARS18.443", &field_type, &field_rev)
},
NumberFormat::UruguayanPeso => {
assert_number(&type_option, "18443", "UYU18.443", &field_type, &field_rev)
},
NumberFormat::Percent => {
assert_number(&type_option, "18443", "18,443%", &field_type, &field_rev)
},
}
}
}
/// Format the input String to the corresponding format string.
#[test]
fn number_type_option_format_str_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(field_type.clone()).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_number(&type_option, "18443", "18443", &field_type, &field_rev);
assert_number(&type_option, "0.2", "0.2", &field_type, &field_rev);
assert_number(&type_option, "", "", &field_type, &field_rev);
assert_number(&type_option, "abc", "", &field_type, &field_rev);
},
NumberFormat::USD => {
assert_number(&type_option, "$18,44", "$1,844", &field_type, &field_rev);
assert_number(&type_option, "$0.2", "$0.2", &field_type, &field_rev);
assert_number(&type_option, "$1844", "$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "$1,844", &field_type, &field_rev);
},
NumberFormat::CanadianDollar => {
assert_number(
&type_option,
"CA$18,44",
"CA$1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "CA$0.2", "CA$0.2", &field_type, &field_rev);
assert_number(&type_option, "CA$1844", "CA$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "CA$1,844", &field_type, &field_rev);
},
NumberFormat::EUR => {
assert_number(&type_option, "€18.44", "€18,44", &field_type, &field_rev);
assert_number(&type_option, "€0.5", "€0,5", &field_type, &field_rev);
assert_number(&type_option, "€1844", "€1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "€1.844", &field_type, &field_rev);
},
NumberFormat::Pound => {
assert_number(&type_option, "£18,44", "£1,844", &field_type, &field_rev);
assert_number(&type_option, "£0.2", "£0.2", &field_type, &field_rev);
assert_number(&type_option, "£1844", "£1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "£1,844", &field_type, &field_rev);
},
NumberFormat::Yen => {
assert_number(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev);
assert_number(&type_option, "¥0.2", "¥0.2", &field_type, &field_rev);
assert_number(&type_option, "¥1844", "¥1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "¥1,844", &field_type, &field_rev);
},
NumberFormat::Ruble => {
assert_number(
&type_option,
"RUB18.44",
"18,44RUB",
&field_type,
&field_rev,
);
assert_number(&type_option, "0.5", "0,5RUB", &field_type, &field_rev);
assert_number(&type_option, "RUB1844", "1.844RUB", &field_type, &field_rev);
assert_number(&type_option, "1844", "1.844RUB", &field_type, &field_rev);
},
NumberFormat::Rupee => {
assert_number(&type_option, "₹18,44", "₹1,844", &field_type, &field_rev);
assert_number(&type_option, "₹0.2", "₹0.2", &field_type, &field_rev);
assert_number(&type_option, "₹1844", "₹1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "₹1,844", &field_type, &field_rev);
},
NumberFormat::Won => {
assert_number(&type_option, "₩18,44", "₩1,844", &field_type, &field_rev);
assert_number(&type_option, "₩0.3", "₩0", &field_type, &field_rev);
assert_number(&type_option, "₩1844", "₩1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "₩1,844", &field_type, &field_rev);
},
NumberFormat::Yuan => {
assert_number(
&type_option,
"CN¥18,44",
"CN¥1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "CN¥0.2", "CN¥0.2", &field_type, &field_rev);
assert_number(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "CN¥1,844", &field_type, &field_rev);
},
NumberFormat::Real => {
assert_number(&type_option, "R$18,44", "R$1,844", &field_type, &field_rev);
assert_number(&type_option, "R$0.2", "R$0.2", &field_type, &field_rev);
assert_number(&type_option, "R$1844", "R$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "R$1,844", &field_type, &field_rev);
},
NumberFormat::Lira => {
assert_number(
&type_option,
"TRY18.44",
"TRY18,44",
&field_type,
&field_rev,
);
assert_number(&type_option, "TRY0.5", "TRY0,5", &field_type, &field_rev);
assert_number(&type_option, "TRY1844", "TRY1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "TRY1.844", &field_type, &field_rev);
},
NumberFormat::Rupiah => {
assert_number(
&type_option,
"IDR18,44",
"IDR1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "IDR0.2", "IDR0.2", &field_type, &field_rev);
assert_number(&type_option, "IDR1844", "IDR1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "IDR1,844", &field_type, &field_rev);
},
NumberFormat::Franc => {
assert_number(
&type_option,
"CHF18,44",
"CHF1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "CHF0.2", "CHF0.2", &field_type, &field_rev);
assert_number(&type_option, "CHF1844", "CHF1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "CHF1,844", &field_type, &field_rev);
},
NumberFormat::HongKongDollar => {
assert_number(
&type_option,
"HZ$18,44",
"HZ$1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "HZ$0.2", "HZ$0.2", &field_type, &field_rev);
assert_number(&type_option, "HZ$1844", "HZ$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "HZ$1,844", &field_type, &field_rev);
},
NumberFormat::NewZealandDollar => {
assert_number(
&type_option,
"NZ$18,44",
"NZ$1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "NZ$0.2", "NZ$0.2", &field_type, &field_rev);
assert_number(&type_option, "NZ$1844", "NZ$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "NZ$1,844", &field_type, &field_rev);
},
NumberFormat::Krona => {
assert_number(
&type_option,
"SEK18,44",
"18,44SEK",
&field_type,
&field_rev,
);
assert_number(&type_option, "SEK0.2", "0,2SEK", &field_type, &field_rev);
assert_number(&type_option, "SEK1844", "1 844SEK", &field_type, &field_rev);
assert_number(&type_option, "1844", "1 844SEK", &field_type, &field_rev);
},
NumberFormat::NorwegianKrone => {
assert_number(
&type_option,
"NOK18,44",
"1,844NOK",
&field_type,
&field_rev,
);
assert_number(&type_option, "NOK0.2", "0.2NOK", &field_type, &field_rev);
assert_number(&type_option, "NOK1844", "1,844NOK", &field_type, &field_rev);
assert_number(&type_option, "1844", "1,844NOK", &field_type, &field_rev);
},
NumberFormat::MexicanPeso => {
assert_number(
&type_option,
"MX$18,44",
"MX$1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "MX$0.2", "MX$0.2", &field_type, &field_rev);
assert_number(&type_option, "MX$1844", "MX$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "MX$1,844", &field_type, &field_rev);
},
NumberFormat::Rand => {
assert_number(
&type_option,
"ZAR18,44",
"ZAR1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "ZAR0.2", "ZAR0.2", &field_type, &field_rev);
assert_number(&type_option, "ZAR1844", "ZAR1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "ZAR1,844", &field_type, &field_rev);
},
NumberFormat::NewTaiwanDollar => {
assert_number(
&type_option,
"NT$18,44",
"NT$1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "NT$0.2", "NT$0.2", &field_type, &field_rev);
assert_number(&type_option, "NT$1844", "NT$1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "NT$1,844", &field_type, &field_rev);
},
NumberFormat::DanishKrone => {
assert_number(
&type_option,
"DKK18.44",
"18,44DKK",
&field_type,
&field_rev,
);
assert_number(&type_option, "DKK0.5", "0,5DKK", &field_type, &field_rev);
assert_number(&type_option, "DKK1844", "1.844DKK", &field_type, &field_rev);
assert_number(&type_option, "1844", "1.844DKK", &field_type, &field_rev);
},
NumberFormat::Baht => {
assert_number(
&type_option,
"THB18,44",
"THB1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "THB0.2", "THB0.2", &field_type, &field_rev);
assert_number(&type_option, "THB1844", "THB1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "THB1,844", &field_type, &field_rev);
},
NumberFormat::Forint => {
assert_number(&type_option, "HUF18,44", "18HUF", &field_type, &field_rev);
assert_number(&type_option, "HUF0.3", "0HUF", &field_type, &field_rev);
assert_number(&type_option, "HUF1844", "1 844HUF", &field_type, &field_rev);
assert_number(&type_option, "1844", "1 844HUF", &field_type, &field_rev);
},
NumberFormat::Koruna => {
assert_number(
&type_option,
"CZK18,44",
"18,44CZK",
&field_type,
&field_rev,
);
assert_number(&type_option, "CZK0.2", "0,2CZK", &field_type, &field_rev);
assert_number(&type_option, "CZK1844", "1 844CZK", &field_type, &field_rev);
assert_number(&type_option, "1844", "1 844CZK", &field_type, &field_rev);
},
NumberFormat::Shekel => {
assert_number(&type_option, "Kč18,44", "18,44Kč", &field_type, &field_rev);
assert_number(&type_option, "Kč0.2", "0,2Kč", &field_type, &field_rev);
assert_number(&type_option, "Kč1844", "1 844Kč", &field_type, &field_rev);
assert_number(&type_option, "1844", "1 844Kč", &field_type, &field_rev);
},
NumberFormat::ChileanPeso => {
assert_number(&type_option, "CLP18.44", "CLP18", &field_type, &field_rev);
assert_number(&type_option, "0.5", "CLP0", &field_type, &field_rev);
assert_number(&type_option, "CLP1844", "CLP1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "CLP1.844", &field_type, &field_rev);
},
NumberFormat::PhilippinePeso => {
assert_number(&type_option, "₱18,44", "₱1,844", &field_type, &field_rev);
assert_number(&type_option, "₱0.2", "₱0.2", &field_type, &field_rev);
assert_number(&type_option, "₱1844", "₱1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "₱1,844", &field_type, &field_rev);
},
NumberFormat::Dirham => {
assert_number(
&type_option,
"AED18,44",
"1,844AED",
&field_type,
&field_rev,
);
assert_number(&type_option, "AED0.2", "0.2AED", &field_type, &field_rev);
assert_number(&type_option, "AED1844", "1,844AED", &field_type, &field_rev);
assert_number(&type_option, "1844", "1,844AED", &field_type, &field_rev);
},
NumberFormat::ColombianPeso => {
assert_number(
&type_option,
"COP18.44",
"COP18,44",
&field_type,
&field_rev,
);
assert_number(&type_option, "0.5", "COP0,5", &field_type, &field_rev);
assert_number(&type_option, "COP1844", "COP1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "COP1.844", &field_type, &field_rev);
},
NumberFormat::Riyal => {
assert_number(
&type_option,
"SAR18,44",
"SAR1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "SAR0.2", "SAR0.2", &field_type, &field_rev);
assert_number(&type_option, "SAR1844", "SAR1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "SAR1,844", &field_type, &field_rev);
},
NumberFormat::Ringgit => {
assert_number(
&type_option,
"MYR18,44",
"MYR1,844",
&field_type,
&field_rev,
);
assert_number(&type_option, "MYR0.2", "MYR0.2", &field_type, &field_rev);
assert_number(&type_option, "MYR1844", "MYR1,844", &field_type, &field_rev);
assert_number(&type_option, "1844", "MYR1,844", &field_type, &field_rev);
},
NumberFormat::Leu => {
assert_number(
&type_option,
"RON18.44",
"18,44RON",
&field_type,
&field_rev,
);
assert_number(&type_option, "0.5", "0,5RON", &field_type, &field_rev);
assert_number(&type_option, "RON1844", "1.844RON", &field_type, &field_rev);
assert_number(&type_option, "1844", "1.844RON", &field_type, &field_rev);
},
NumberFormat::ArgentinePeso => {
assert_number(
&type_option,
"ARS18.44",
"ARS18,44",
&field_type,
&field_rev,
);
assert_number(&type_option, "0.5", "ARS0,5", &field_type, &field_rev);
assert_number(&type_option, "ARS1844", "ARS1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "ARS1.844", &field_type, &field_rev);
},
NumberFormat::UruguayanPeso => {
assert_number(
&type_option,
"UYU18.44",
"UYU18,44",
&field_type,
&field_rev,
);
assert_number(&type_option, "0.5", "UYU0,5", &field_type, &field_rev);
assert_number(&type_option, "UYU1844", "UYU1.844", &field_type, &field_rev);
assert_number(&type_option, "1844", "UYU1.844", &field_type, &field_rev);
},
NumberFormat::Percent => {
assert_number(&type_option, "1", "1%", &field_type, &field_rev);
assert_number(&type_option, "10.1", "10.1%", &field_type, &field_rev);
assert_number(&type_option, "100", "100%", &field_type, &field_rev);
},
}
}
}
/// Carry out the sign positive to input number
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(field_type.clone()).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_number(&type_option, "18443", "18443", &field_type, &field_rev);
},
NumberFormat::USD => {
assert_number(&type_option, "18443", "-$18,443", &field_type, &field_rev);
},
NumberFormat::CanadianDollar => {
assert_number(&type_option, "18443", "-CA$18,443", &field_type, &field_rev)
},
NumberFormat::EUR => {
assert_number(&type_option, "18443", "-€18.443", &field_type, &field_rev)
},
NumberFormat::Pound => {
assert_number(&type_option, "18443", "-£18,443", &field_type, &field_rev)
},
NumberFormat::Yen => {
assert_number(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
},
NumberFormat::Ruble => {
assert_number(&type_option, "18443", "-18.443RUB", &field_type, &field_rev)
},
NumberFormat::Rupee => {
assert_number(&type_option, "18443", "-₹18,443", &field_type, &field_rev)
},
NumberFormat::Won => {
assert_number(&type_option, "18443", "-₩18,443", &field_type, &field_rev)
},
NumberFormat::Yuan => {
assert_number(&type_option, "18443", "-CN¥18,443", &field_type, &field_rev);
},
NumberFormat::Real => {
assert_number(&type_option, "18443", "-R$18,443", &field_type, &field_rev);
},
NumberFormat::Lira => {
assert_number(&type_option, "18443", "-TRY18.443", &field_type, &field_rev)
},
NumberFormat::Rupiah => {
assert_number(&type_option, "18443", "-IDR18,443", &field_type, &field_rev)
},
NumberFormat::Franc => {
assert_number(&type_option, "18443", "-CHF18,443", &field_type, &field_rev)
},
NumberFormat::HongKongDollar => {
assert_number(&type_option, "18443", "-HZ$18,443", &field_type, &field_rev)
},
NumberFormat::NewZealandDollar => {
assert_number(&type_option, "18443", "-NZ$18,443", &field_type, &field_rev)
},
NumberFormat::Krona => {
assert_number(&type_option, "18443", "-18 443SEK", &field_type, &field_rev)
},
NumberFormat::NorwegianKrone => {
assert_number(&type_option, "18443", "-18,443NOK", &field_type, &field_rev)
},
NumberFormat::MexicanPeso => {
assert_number(&type_option, "18443", "-MX$18,443", &field_type, &field_rev)
},
NumberFormat::Rand => {
assert_number(&type_option, "18443", "-ZAR18,443", &field_type, &field_rev)
},
NumberFormat::NewTaiwanDollar => {
assert_number(&type_option, "18443", "-NT$18,443", &field_type, &field_rev)
},
NumberFormat::DanishKrone => {
assert_number(&type_option, "18443", "-18.443DKK", &field_type, &field_rev)
},
NumberFormat::Baht => {
assert_number(&type_option, "18443", "-THB18,443", &field_type, &field_rev)
},
NumberFormat::Forint => {
assert_number(&type_option, "18443", "-18 443HUF", &field_type, &field_rev)
},
NumberFormat::Koruna => {
assert_number(&type_option, "18443", "-18 443CZK", &field_type, &field_rev)
},
NumberFormat::Shekel => {
assert_number(&type_option, "18443", "-18 443Kč", &field_type, &field_rev)
},
NumberFormat::ChileanPeso => {
assert_number(&type_option, "18443", "-CLP18.443", &field_type, &field_rev)
},
NumberFormat::PhilippinePeso => {
assert_number(&type_option, "18443", "-₱18,443", &field_type, &field_rev)
},
NumberFormat::Dirham => {
assert_number(&type_option, "18443", "-18,443AED", &field_type, &field_rev)
},
NumberFormat::ColombianPeso => {
assert_number(&type_option, "18443", "-COP18.443", &field_type, &field_rev)
},
NumberFormat::Riyal => {
assert_number(&type_option, "18443", "-SAR18,443", &field_type, &field_rev)
},
NumberFormat::Ringgit => {
assert_number(&type_option, "18443", "-MYR18,443", &field_type, &field_rev)
},
NumberFormat::Leu => {
assert_number(&type_option, "18443", "-18.443RON", &field_type, &field_rev)
},
NumberFormat::ArgentinePeso => {
assert_number(&type_option, "18443", "-ARS18.443", &field_type, &field_rev)
},
NumberFormat::UruguayanPeso => {
assert_number(&type_option, "18443", "-UYU18.443", &field_type, &field_rev)
},
NumberFormat::Percent => {
assert_number(&type_option, "18443", "-18,443%", &field_type, &field_rev)
},
}
}
}
fn assert_number(
type_option: &NumberTypeOption,
input_str: &str,
expected_str: &str,
field_type: &FieldType,
field_rev: &Field,
) {
assert_eq!(
type_option
.decode_cell_str(
&NumberCellData(input_str.to_owned()).into(),
field_type,
field_rev
)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,277 @@
use crate::entities::{FieldType, NumberFilterPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{
NumberCellFormat, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, CELL_DATE,
};
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use crate::services::field::type_options::util::ProtobufStr;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use fancy_regex::Regex;
use flowy_error::FlowyResult;
use lazy_static::lazy_static;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::default::Default;
use std::str::FromStr;
// Number
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NumberTypeOption {
pub format: NumberFormat,
pub scale: u32,
pub symbol: String,
pub sign_positive: bool,
pub name: String,
}
#[derive(Clone, Debug, Default)]
pub struct NumberCellData(pub String);
impl From<&Cell> for NumberCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATE).unwrap_or_default())
}
}
impl From<NumberCellData> for Cell {
fn from(data: NumberCellData) -> Self {
new_cell_builder(FieldType::Number)
.insert_str_value(CELL_DATE, data.0)
.build()
}
}
impl std::convert::From<String> for NumberCellData {
fn from(s: String) -> Self {
Self(s)
}
}
impl ToString for NumberCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl TypeOption for NumberTypeOption {
type CellData = NumberCellData;
type CellChangeset = NumberCellChangeset;
type CellProtobufType = ProtobufStr;
type CellFilter = NumberFilterPB;
}
impl From<TypeOptionData> for NumberTypeOption {
fn from(data: TypeOptionData) -> Self {
let format = data
.get_i64_value("format")
.map(NumberFormat::from)
.unwrap_or_default();
let scale = data.get_i64_value("scale").unwrap_or_default() as u32;
let symbol = data.get_str_value("symbol").unwrap_or_default();
let sign_positive = data.get_bool_value("sign_positive").unwrap_or_default();
let name = data.get_str_value("name").unwrap_or_default();
Self {
format,
scale,
symbol,
sign_positive,
name,
}
}
}
impl From<NumberTypeOption> for TypeOptionData {
fn from(data: NumberTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_i64_value("format", data.format.value())
.insert_i64_value("scale", data.scale as i64)
.insert_bool_value("sign_positive", data.sign_positive)
.insert_str_value("name", data.name)
.insert_str_value("symbol", data.symbol)
.build()
}
}
impl TypeOptionCellData for NumberTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(NumberCellData::from(cell))
}
}
impl NumberTypeOption {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn format_cell_data(
&self,
num_cell_data: &NumberCellData,
) -> FlowyResult<NumberCellFormat> {
match self.format {
NumberFormat::Num => {
if SCIENTIFIC_NOTATION_REGEX
.is_match(&num_cell_data.0)
.unwrap()
{
match Decimal::from_scientific(&num_cell_data.0.to_lowercase()) {
Ok(value, ..) => Ok(NumberCellFormat::from_decimal(value)),
Err(_) => Ok(NumberCellFormat::new()),
}
} else {
let draw_numer_string = NUM_REGEX.replace_all(&num_cell_data.0, "");
let strnum = match draw_numer_string.matches('.').count() {
0 | 1 => draw_numer_string.to_string(),
_ => match EXTRACT_NUM_REGEX.captures(&draw_numer_string) {
Ok(captures) => match captures {
Some(capture) => capture[1].to_string(),
None => "".to_string(),
},
Err(_) => "".to_string(),
},
};
match Decimal::from_str(&strnum) {
Ok(value, ..) => Ok(NumberCellFormat::from_decimal(value)),
Err(_) => Ok(NumberCellFormat::new()),
}
}
},
_ => NumberCellFormat::from_format_str(&num_cell_data.0, self.sign_positive, &self.format),
}
}
pub fn set_format(&mut self, format: NumberFormat) {
self.format = format;
self.symbol = format.symbol();
}
}
pub(crate) fn strip_currency_symbol<T: ToString>(s: T) -> String {
let mut s = s.to_string();
for symbol in CURRENCY_SYMBOL.iter() {
if s.starts_with(symbol) {
s = s.strip_prefix(symbol).unwrap_or("").to_string();
break;
}
}
s
}
impl TypeOptionTransform for NumberTypeOption {}
impl CellDataDecoder for NumberTypeOption {
fn decode_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if decoded_field_type.is_date() {
return Ok(Default::default());
}
let num_cell_data = self.decode_cell(cell)?;
Ok(NumberCellData::from(
self.format_cell_data(&num_cell_data)?.to_string(),
))
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
match self.format_cell_data(&cell_data) {
Ok(cell_data) => cell_data.to_string(),
Err(_) => "".to_string(),
}
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
}
}
pub type NumberCellChangeset = String;
impl CellDataChangeset for NumberTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let number_cell_data = NumberCellData(changeset.trim().to_string());
let formatter = self.format_cell_data(&number_cell_data)?;
match self.format {
NumberFormat::Num => Ok((
NumberCellData(formatter.to_string()).into(),
NumberCellData::from(formatter.to_string()),
)),
_ => Ok((
NumberCellData::from(formatter.to_string()).into(),
NumberCellData::from(formatter.to_string()),
)),
}
}
}
impl TypeOptionCellDataFilter for NumberTypeOption {
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 NumberTypeOption {
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 NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();
let symbol = format.symbol();
NumberTypeOption {
format,
scale: 0,
symbol,
sign_positive: true,
name: "Number".to_string(),
}
}
}
lazy_static! {
static ref NUM_REGEX: Regex = Regex::new(r"[^\d\.]").unwrap();
}
lazy_static! {
static ref SCIENTIFIC_NOTATION_REGEX: Regex = Regex::new(r"([+-]?\d*\.?\d+)e([+-]?\d+)").unwrap();
}
lazy_static! {
static ref EXTRACT_NUM_REGEX: Regex = Regex::new(r"^(\d+\.\d+)(?:\.\d+)*$").unwrap();
}

View File

@ -0,0 +1,126 @@
use crate::services::cell::{CellBytesCustomParser, CellProtobufBlobParser, DecodedCellData};
use crate::services::field::number_currency::Currency;
use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL};
use bytes::Bytes;
use flowy_error::FlowyResult;
use rust_decimal::Decimal;
use rusty_money::Money;
use std::str::FromStr;
#[derive(Default)]
pub struct NumberCellFormat {
decimal: Option<Decimal>,
money: Option<String>,
}
impl NumberCellFormat {
pub fn new() -> Self {
Self {
decimal: Default::default(),
money: None,
}
}
pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult<Self> {
let mut num_str = strip_currency_symbol(s);
let currency = format.currency();
if num_str.is_empty() {
return Ok(Self::default());
}
match Decimal::from_str(&num_str) {
Ok(mut decimal) => {
decimal.set_sign_positive(sign_positive);
let money = Money::from_decimal(decimal, currency);
Ok(Self::from_money(money))
},
Err(_) => match Money::from_str(&num_str, currency) {
Ok(money) => Ok(NumberCellFormat::from_money(money)),
Err(_) => {
num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format)
} else {
// returns empty string if it can be formatted
Ok(Self::default())
}
},
},
}
}
pub fn from_decimal(decimal: Decimal) -> Self {
Self {
decimal: Some(decimal),
money: None,
}
}
pub fn from_money(money: Money<Currency>) -> Self {
Self {
decimal: Some(*money.amount()),
money: Some(money.to_string()),
}
}
pub fn decimal(&self) -> &Option<Decimal> {
&self.decimal
}
pub fn is_empty(&self) -> bool {
self.decimal.is_none()
}
}
// impl FromStr for NumberCellData {
// type Err = FlowyError;
//
// fn from_str(s: &str) -> Result<Self, Self::Err> {
// if s.is_empty() {
// return Ok(Self::default());
// }
// let decimal = Decimal::from_str(s).map_err(internal_error)?;
// Ok(Self::from_decimal(decimal))
// }
// }
impl ToString for NumberCellFormat {
fn to_string(&self) -> String {
match &self.money {
None => match self.decimal {
None => String::default(),
Some(decimal) => decimal.to_string(),
},
Some(money) => money.to_string(),
}
}
}
impl DecodedCellData for NumberCellFormat {
type Object = NumberCellFormat;
fn is_empty(&self) -> bool {
self.decimal.is_none()
}
}
pub struct NumberCellDataParser();
impl CellProtobufBlobParser for NumberCellDataParser {
type Object = NumberCellFormat;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => NumberCellFormat::from_format_str(&s, true, &NumberFormat::Num),
Err(_) => Ok(NumberCellFormat::default()),
}
}
}
pub struct NumberCellCustomDataParser(pub NumberFormat);
impl CellBytesCustomParser for NumberCellCustomDataParser {
type Object = NumberCellFormat;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => NumberCellFormat::from_format_str(&s, true, &self.0),
Err(_) => Ok(NumberCellFormat::default()),
}
}
}

View File

@ -0,0 +1,40 @@
use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB};
use crate::services::field::{SelectOption, SelectedSelectOptions};
impl ChecklistFilterPB {
pub fn is_visible(
&self,
all_options: &[SelectOption],
selected_options: &SelectedSelectOptions,
) -> bool {
let selected_option_ids = selected_options
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<&str>>();
let mut all_option_ids = all_options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<&str>>();
match self.condition {
ChecklistFilterConditionPB::IsComplete => {
if selected_option_ids.is_empty() {
return false;
}
all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id));
all_option_ids.is_empty()
},
ChecklistFilterConditionPB::IsIncomplete => {
if selected_option_ids.is_empty() {
return true;
}
all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id));
!all_option_ids.is_empty()
},
}
}
}

View File

@ -0,0 +1,143 @@
use crate::entities::{ChecklistFilterPB, FieldType, SelectOptionCellDataPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
SelectOption, SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction,
SelectedSelectOptions, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter,
};
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
// Multiple select
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ChecklistTypeOption {
pub options: Vec<SelectOption>,
pub disable_color: bool,
}
impl TypeOption for ChecklistTypeOption {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
type CellFilter = ChecklistFilterPB;
}
impl From<TypeOptionData> for ChecklistTypeOption {
fn from(data: TypeOptionData) -> Self {
data
.get_str_value("content")
.map(|s| serde_json::from_str::<ChecklistTypeOption>(&s).unwrap_or_default())
.unwrap_or_default()
}
}
impl From<ChecklistTypeOption> for TypeOptionData {
fn from(data: ChecklistTypeOption) -> Self {
let content = serde_json::to_string(&data).unwrap_or_default();
TypeOptionDataBuilder::new()
.insert_str_value("content", content)
.build()
}
}
impl TypeOptionCellData for ChecklistTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.get_selected_options(cell_data).into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SelectOptionIds::from(cell))
}
}
impl SelectTypeOptionSharedAction for ChecklistTypeOption {
fn number_of_max_options(&self) -> Option<usize> {
None
}
fn to_type_option_data(&self) -> TypeOptionData {
self.clone().into()
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellDataChangeset for ChecklistTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <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 select_option_ids = match cell {
None => SelectOptionIds::from(insert_option_ids),
Some(cell) => {
let mut select_ids = SelectOptionIds::from(&cell);
for insert_option_id in insert_option_ids {
if !select_ids.contains(&insert_option_id) {
select_ids.push(insert_option_id);
}
}
for delete_option_id in changeset.delete_option_ids {
select_ids.retain(|id| id != &delete_option_id);
}
select_ids
},
};
Ok((
select_option_ids.to_cell_data(FieldType::Checklist),
select_option_ids,
))
}
}
impl TypeOptionCellDataFilter for ChecklistTypeOption {
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 ChecklistTypeOption {
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())
}
}

View File

@ -0,0 +1,17 @@
mod checklist_filter;
mod checklist_type_option;
mod multi_select_type_option;
mod select_filter;
mod select_ids;
mod select_option;
mod select_type_option;
mod single_select_type_option;
mod type_option_transform;
pub use checklist_filter::*;
pub use checklist_type_option::*;
pub use multi_select_type_option::*;
pub use select_ids::*;
pub use select_option::*;
pub use select_type_option::*;
pub use single_select_type_option::*;

View File

@ -0,0 +1,288 @@
use std::cmp::{min, Ordering};
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use serde::{Deserialize, Serialize};
use flowy_error::FlowyResult;
use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, SelectOptionCellChangeset, SelectOptionIds,
SelectTypeOptionSharedAction, SelectedSelectOptions, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter,
};
// Multiple select
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct MultiSelectTypeOption {
pub options: Vec<SelectOption>,
pub disable_color: bool,
}
impl TypeOption for MultiSelectTypeOption {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
type CellFilter = SelectOptionFilterPB;
}
impl From<TypeOptionData> for MultiSelectTypeOption {
fn from(data: TypeOptionData) -> Self {
data
.get_str_value("content")
.map(|s| serde_json::from_str::<MultiSelectTypeOption>(&s).unwrap_or_default())
.unwrap_or_default()
}
}
impl From<MultiSelectTypeOption> for TypeOptionData {
fn from(data: MultiSelectTypeOption) -> Self {
let content = serde_json::to_string(&data).unwrap_or_default();
TypeOptionDataBuilder::new()
.insert_str_value("content", content)
.build()
}
}
impl TypeOptionCellData for MultiSelectTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.get_selected_options(cell_data).into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SelectOptionIds::from(cell))
}
}
impl SelectTypeOptionSharedAction for MultiSelectTypeOption {
fn number_of_max_options(&self) -> Option<usize> {
None
}
fn to_type_option_data(&self) -> TypeOptionData {
self.clone().into()
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellDataChangeset for MultiSelectTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <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 select_option_ids = match cell {
None => SelectOptionIds::from(insert_option_ids),
Some(cell) => {
let mut select_ids = SelectOptionIds::from(&cell);
for insert_option_id in insert_option_ids {
if !select_ids.contains(&insert_option_id) {
select_ids.push(insert_option_id);
}
}
for delete_option_id in changeset.delete_option_ids {
select_ids.retain(|id| id != &delete_option_id);
}
tracing::trace!("Multi-select cell data: {}", select_ids.to_string());
select_ids
},
};
Ok((
select_option_ids.to_cell_data(FieldType::MultiSelect),
select_option_ids,
))
}
}
impl TypeOptionCellDataFilter for MultiSelectTypeOption {
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 MultiSelectTypeOption {
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()
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataChangeset;
use crate::services::field::type_options::selection_type_option::*;
use crate::services::field::MultiSelectTypeOption;
use crate::services::field::{CheckboxTypeOption, TypeOptionTransform};
#[test]
fn multi_select_transform_with_checkbox_type_option_test() {
let checkbox_type_option = CheckboxTypeOption { is_selected: false };
let mut multi_select = MultiSelectTypeOption::default();
multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.clone().into());
debug_assert_eq!(multi_select.options.len(), 2);
// Already contain the yes/no option. It doesn't need to insert new options
multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.into());
debug_assert_eq!(multi_select.options.len(), 2);
}
#[test]
fn multi_select_transform_with_single_select_type_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let single_select = SingleSelectTypeOption {
options: vec![google, facebook],
disable_color: false,
};
let mut multi_select = MultiSelectTypeOption {
options: vec![],
disable_color: false,
};
multi_select.transform_type_option(FieldType::MultiSelect, single_select.into());
debug_assert_eq!(multi_select.options.len(), 2);
}
// #[test]
#[test]
fn multi_select_insert_multi_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let multi_select = MultiSelectTypeOption {
options: vec![google.clone(), facebook.clone()],
disable_color: false,
};
let option_ids = vec![google.id, facebook.id];
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids: SelectOptionIds =
multi_select.apply_changeset(changeset, None).unwrap().1;
assert_eq!(&*select_option_ids, &option_ids);
}
#[test]
fn multi_select_unselect_multi_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let multi_select = MultiSelectTypeOption {
options: vec![google.clone(), facebook.clone()],
disable_color: false,
};
let option_ids = vec![google.id, facebook.id];
// insert
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1;
assert_eq!(&*select_option_ids, &option_ids);
// delete
let changeset = SelectOptionCellChangeset::from_delete_options(option_ids);
let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1;
assert!(select_option_ids.is_empty());
}
#[test]
fn multi_select_insert_single_option_test() {
let google = SelectOption::new("Google");
let multi_select = MultiSelectTypeOption {
options: vec![google.clone()],
disable_color: false,
};
let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id);
let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1;
assert_eq!(select_option_ids.to_string(), google.id);
}
#[test]
fn multi_select_insert_non_exist_option_test() {
let google = SelectOption::new("Google");
let multi_select = MultiSelectTypeOption {
options: vec![],
disable_color: false,
};
let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id);
let (_, select_option_ids) = multi_select.apply_changeset(changeset, None).unwrap();
assert!(select_option_ids.is_empty());
}
#[test]
fn multi_select_insert_invalid_option_id_test() {
let google = SelectOption::new("Google");
let multi_select = MultiSelectTypeOption {
options: vec![google],
disable_color: false,
};
// empty option id string
let changeset = SelectOptionCellChangeset::from_insert_option_id("");
let (cell, _) = multi_select.apply_changeset(changeset, None).unwrap();
let option_ids = SelectOptionIds::from(&cell);
assert!(option_ids.is_empty());
let changeset = SelectOptionCellChangeset::from_insert_option_id("123,456");
let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1;
assert!(select_option_ids.is_empty());
}
}

View File

@ -0,0 +1,316 @@
#![allow(clippy::needless_collect)]
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 {
let selected_option_ids: Vec<&String> = selected_options
.options
.iter()
.map(|option| &option.id)
.collect();
match self.condition {
SelectOptionConditionPB::OptionIs => match field_type {
FieldType::SingleSelect => {
if self.option_ids.is_empty() {
return true;
}
if selected_options.options.is_empty() {
return false;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
!required_options.is_empty()
},
FieldType::MultiSelect => {
if self.option_ids.is_empty() {
return true;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
!required_options.is_empty()
},
_ => false,
},
SelectOptionConditionPB::OptionIsNot => match field_type {
FieldType::SingleSelect => {
if self.option_ids.is_empty() {
return true;
}
if selected_options.options.is_empty() {
return false;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.is_empty()
},
FieldType::MultiSelect => {
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.is_empty()
},
_ => false,
},
SelectOptionConditionPB::OptionIsEmpty => selected_option_ids.is_empty(),
SelectOptionConditionPB::OptionIsNotEmpty => !selected_option_ids.is_empty(),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]
use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use crate::services::field::selection_type_option::SelectedSelectOptions;
use crate::services::field::SelectOption;
#[test]
fn select_option_filter_is_empty_test() {
let option = SelectOption::new("A");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
};
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::SingleSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option.clone()]
},
FieldType::SingleSelect
),
false,
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::MultiSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option]
},
FieldType::MultiSelect
),
false,
);
}
#[test]
fn select_option_filter_is_not_empty_test() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsNotEmpty,
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
};
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option_1.clone()]
},
FieldType::SingleSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::SingleSelect
),
false,
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option_1.clone()]
},
FieldType::MultiSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::MultiSelect
),
false,
);
}
#[test]
fn single_select_option_filter_is_not_test() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let option_3 = SelectOption::new("C");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsNot,
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
};
for (options, is_visible) in vec![
(vec![option_2.clone()], false),
(vec![option_1.clone()], false),
(vec![option_3.clone()], true),
(vec![option_1.clone(), option_2.clone()], false),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
is_visible
);
}
}
#[test]
fn single_select_option_filter_is_test() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let option_3 = SelectOption::new("c");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![option_1.id.clone()],
};
for (options, is_visible) in vec![
(vec![option_1.clone()], true),
(vec![option_2.clone()], false),
(vec![option_3.clone()], false),
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
is_visible
);
}
}
#[test]
fn single_select_option_filter_is_test2() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![],
};
for (options, is_visible) in vec![
(vec![option_1.clone()], true),
(vec![option_2.clone()], true),
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
is_visible
);
}
}
#[test]
fn multi_select_option_filter_not_contains_test() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let option_3 = SelectOption::new("C");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsNot,
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
};
for (options, is_visible) in vec![
(vec![option_1.clone(), option_2.clone()], false),
(vec![option_1.clone()], false),
(vec![option_2.clone()], false),
(vec![option_3.clone()], true),
(
vec![option_1.clone(), option_2.clone(), option_3.clone()],
false,
),
(vec![], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
is_visible
);
}
}
#[test]
fn multi_select_option_filter_contains_test() {
let option_1 = SelectOption::new("A");
let option_2 = SelectOption::new("B");
let option_3 = SelectOption::new("C");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
};
for (options, is_visible) in vec![
(
vec![option_1.clone(), option_2.clone(), option_3.clone()],
true,
),
(vec![option_2.clone(), option_1.clone()], true),
(vec![option_2.clone()], true),
(vec![option_1.clone(), option_3.clone()], true),
(vec![option_3.clone()], false),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
is_visible
);
}
}
#[test]
fn multi_select_option_filter_contains_test2() {
let option_1 = SelectOption::new("A");
let filter = SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![],
};
for (options, is_visible) in vec![(vec![option_1.clone()], true), (vec![], true)] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
is_visible
);
}
}
}

View File

@ -0,0 +1,110 @@
use crate::entities::FieldType;
use crate::services::cell::{DecodedCellData, FromCellString};
use crate::services::field::CELL_DATE;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::FlowyResult;
pub const SELECTION_IDS_SEPARATOR: &str = ",";
/// List of select option ids
///
/// Calls [to_string] will return a string consists list of ids,
/// placing a commas separator between each
///
#[derive(Default, Clone, Debug)]
pub struct SelectOptionIds(Vec<String>);
impl SelectOptionIds {
pub fn new() -> Self {
Self::default()
}
pub fn into_inner(self) -> Vec<String> {
self.0
}
pub fn to_cell_data(&self, field_type: FieldType) -> Cell {
new_cell_builder(field_type)
.insert_str_value(CELL_DATE, self.to_string())
.build()
}
}
impl FromCellString for SelectOptionIds {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Ok(Self::from(s.to_owned()))
}
}
impl From<&Cell> for SelectOptionIds {
fn from(cell: &Cell) -> Self {
let value = cell.get_str_value(CELL_DATE).unwrap_or_default();
Self::from(value)
}
}
impl std::convert::From<String> for SelectOptionIds {
fn from(s: String) -> Self {
if s.is_empty() {
return Self(vec![]);
}
let ids = s
.split(SELECTION_IDS_SEPARATOR)
.map(|id| id.to_string())
.collect::<Vec<String>>();
Self(ids)
}
}
impl std::convert::From<Vec<String>> for SelectOptionIds {
fn from(ids: Vec<String>) -> Self {
let ids = ids
.into_iter()
.filter(|id| !id.is_empty())
.collect::<Vec<String>>();
Self(ids)
}
}
impl ToString for SelectOptionIds {
/// Returns a string that consists list of ids, placing a commas
/// separator between each
fn to_string(&self) -> String {
self.0.join(SELECTION_IDS_SEPARATOR)
}
}
impl std::convert::From<Option<String>> for SelectOptionIds {
fn from(s: Option<String>) -> Self {
match s {
None => Self(vec![]),
Some(s) => Self::from(s),
}
}
}
impl std::ops::Deref for SelectOptionIds {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for SelectOptionIds {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl DecodedCellData for SelectOptionIds {
type Object = SelectOptionIds;
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

View File

@ -0,0 +1,97 @@
use crate::entities::SelectOptionCellDataPB;
use crate::services::field::SelectOptionIds;
use collab_database::database::gen_option_id;
use serde::{Deserialize, Serialize};
/// [SelectOption] represents an option for a single select, and multiple select.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelectOption {
pub id: String,
pub name: String,
pub color: SelectOptionColor,
}
impl SelectOption {
pub fn new(name: &str) -> Self {
SelectOption {
id: gen_option_id(),
name: name.to_owned(),
color: SelectOptionColor::default(),
}
}
pub fn with_color(name: &str, color: SelectOptionColor) -> Self {
SelectOption {
id: gen_option_id(),
name: name.to_owned(),
color,
}
}
}
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
#[repr(u8)]
pub enum SelectOptionColor {
Purple = 0,
Pink = 1,
LightPink = 2,
Orange = 3,
Yellow = 4,
Lime = 5,
Green = 6,
Aqua = 7,
Blue = 8,
}
impl std::default::Default for SelectOptionColor {
fn default() -> Self {
SelectOptionColor::Purple
}
}
#[derive(Debug)]
pub struct SelectOptionCellData {
pub options: Vec<SelectOption>,
pub select_options: Vec<SelectOption>,
}
impl From<SelectOptionCellData> for SelectOptionCellDataPB {
fn from(data: SelectOptionCellData) -> Self {
SelectOptionCellDataPB {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
select_options: data
.select_options
.into_iter()
.map(|option| option.into())
.collect(),
}
}
}
pub fn make_selected_options(ids: SelectOptionIds, options: &[SelectOption]) -> Vec<SelectOption> {
ids
.iter()
.flat_map(|option_id| {
options
.iter()
.find(|option| &option.id == option_id)
.cloned()
})
.collect()
}
pub struct SelectedSelectOptions {
pub(crate) options: Vec<SelectOption>,
}
impl std::convert::From<SelectOptionCellData> for SelectedSelectOptions {
fn from(data: SelectOptionCellData) -> Self {
Self {
options: data.select_options,
}
}
}

View File

@ -0,0 +1,281 @@
use bytes::Bytes;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::Cell;
use serde::{Deserialize, Serialize};
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use crate::entities::{FieldType, SelectOptionCellDataPB};
use crate::services::cell::{
CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellChangeset, ToCellChangeset,
};
use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper;
use crate::services::field::{
make_selected_options, CheckboxCellData, ChecklistTypeOption, MultiSelectTypeOption,
SelectOption, SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption,
TypeOption, TypeOptionCellData, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
/// Defines the shared actions used by SingleSelect or Multi-Select.
pub trait SelectTypeOptionSharedAction: Send + Sync {
/// Returns `None` means there is no limited
fn number_of_max_options(&self) -> Option<usize>;
/// Insert the `SelectOption` into corresponding type option.
fn insert_option(&mut self, new_option: SelectOption) {
let options = self.mut_options();
if let Some(index) = options
.iter()
.position(|option| option.id == new_option.id || option.name == new_option.name)
{
options.remove(index);
options.insert(index, new_option);
} else {
options.insert(0, new_option);
}
}
fn delete_option(&mut self, delete_option: SelectOption) {
let options = self.mut_options();
if let Some(index) = options
.iter()
.position(|option| option.id == delete_option.id)
{
options.remove(index);
}
}
fn create_option(&self, name: &str) -> SelectOption {
let color = new_select_option_color(self.options());
SelectOption::with_color(name, color)
}
/// Return a list of options that are selected by user
fn get_selected_options(&self, ids: SelectOptionIds) -> SelectOptionCellData {
let mut select_options = make_selected_options(ids, self.options());
match self.number_of_max_options() {
None => {},
Some(number_of_max_options) => {
select_options.truncate(number_of_max_options);
},
}
SelectOptionCellData {
options: self.options().clone(),
select_options,
}
}
fn to_type_option_data(&self) -> TypeOptionData;
fn options(&self) -> &Vec<SelectOption>;
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
}
impl<T> TypeOptionTransform for T
where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + CellDataDecoder,
{
fn transformable(&self) -> bool {
true
}
fn transform_type_option(
&mut self,
_old_type_option_field_type: FieldType,
_old_type_option_data: TypeOptionData,
) {
SelectOptionTypeOptionTransformHelper::transform_type_option(
self,
&_old_type_option_field_type,
_old_type_option_data,
);
}
fn transform_type_option_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
match _decoded_field_type {
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => None,
FieldType::Checkbox => {
let cell_content = CheckboxCellData::from(cell).to_string();
let mut transformed_ids = Vec::new();
let options = self.options();
if let Some(option) = options.iter().find(|option| option.name == cell_content) {
transformed_ids.push(option.id.clone());
}
Some(SelectOptionIds::from(transformed_ids))
},
FieldType::RichText => Some(SelectOptionIds::from(cell)),
_ => Some(SelectOptionIds::from(vec![])),
}
}
}
impl<T> CellDataDecoder for T
where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellData,
{
fn decode_cell_str(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
self.decode_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
self
.get_selected_options(cell_data)
.select_options
.into_iter()
.map(|option| option.name)
.collect::<Vec<String>>()
.join(SELECTION_IDS_SEPARATOR)
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
}
}
pub fn select_type_option_from_field(
field_rev: &Field,
) -> FlowyResult<Box<dyn SelectTypeOptionSharedAction>> {
let field_type = FieldType::from(field_rev.field_type);
match &field_type {
FieldType::SingleSelect => {
let type_option = field_rev
.get_type_option::<SingleSelectTypeOption>(field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
FieldType::MultiSelect => {
let type_option = field_rev
.get_type_option::<MultiSelectTypeOption>(&field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
FieldType::Checklist => {
let type_option = field_rev
.get_type_option::<ChecklistTypeOption>(&field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
ty => {
tracing::error!("Unsupported field type: {:?} for this handler", ty);
Err(ErrorCode::FieldInvalidOperation.into())
},
}
}
pub fn new_select_option_color(options: &[SelectOption]) -> SelectOptionColor {
let mut freq: Vec<usize> = vec![0; 9];
for option in options {
freq[option.color.to_owned() as usize] += 1;
}
match freq
.into_iter()
.enumerate()
.min_by_key(|(_, v)| *v)
.map(|(idx, _val)| idx)
.unwrap()
{
0 => SelectOptionColor::Purple,
1 => SelectOptionColor::Pink,
2 => SelectOptionColor::LightPink,
3 => SelectOptionColor::Orange,
4 => SelectOptionColor::Yellow,
5 => SelectOptionColor::Lime,
6 => SelectOptionColor::Green,
7 => SelectOptionColor::Aqua,
8 => SelectOptionColor::Blue,
_ => SelectOptionColor::Purple,
}
}
pub struct SelectOptionIdsParser();
impl CellProtobufBlobParser for SelectOptionIdsParser {
type Object = SelectOptionIds;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => Ok(SelectOptionIds::from(s)),
Err(_) => Ok(SelectOptionIds::from("".to_owned())),
}
}
}
impl DecodedCellData for SelectOptionCellDataPB {
type Object = SelectOptionCellDataPB;
fn is_empty(&self) -> bool {
self.select_options.is_empty()
}
}
pub struct SelectOptionCellDataParser();
impl CellProtobufBlobParser for SelectOptionCellDataParser {
type Object = SelectOptionCellDataPB;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
SelectOptionCellDataPB::try_from(bytes.as_ref()).map_err(internal_error)
}
}
#[derive(Clone, Serialize, Deserialize, Default, Debug)]
pub struct SelectOptionCellChangeset {
pub insert_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
}
impl FromCellChangeset for SelectOptionCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<SelectOptionCellChangeset>(&changeset).map_err(internal_error)
}
}
impl ToCellChangeset for SelectOptionCellChangeset {
fn to_cell_changeset_str(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
}
impl SelectOptionCellChangeset {
pub fn from_insert_option_id(option_id: &str) -> Self {
SelectOptionCellChangeset {
insert_option_ids: vec![option_id.to_string()],
delete_option_ids: vec![],
}
}
pub fn from_insert_options(option_ids: Vec<String>) -> Self {
SelectOptionCellChangeset {
insert_option_ids: option_ids,
delete_option_ids: vec![],
}
}
pub fn from_delete_option_id(option_id: &str) -> Self {
SelectOptionCellChangeset {
insert_option_ids: vec![],
delete_option_ids: vec![option_id.to_string()],
}
}
pub fn from_delete_options(option_ids: Vec<String>) -> Self {
SelectOptionCellChangeset {
insert_option_ids: vec![],
delete_option_ids: option_ids,
}
}
}

View File

@ -0,0 +1,224 @@
use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, SelectedSelectOptions, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter,
};
use crate::services::field::{
SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction,
};
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
// Single select
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SingleSelectTypeOption {
pub options: Vec<SelectOption>,
pub disable_color: bool,
}
impl TypeOption for SingleSelectTypeOption {
type CellData = SelectOptionIds;
type CellChangeset = SelectOptionCellChangeset;
type CellProtobufType = SelectOptionCellDataPB;
type CellFilter = SelectOptionFilterPB;
}
impl From<TypeOptionData> for SingleSelectTypeOption {
fn from(data: TypeOptionData) -> Self {
data
.get_str_value("content")
.map(|s| serde_json::from_str::<SingleSelectTypeOption>(&s).unwrap_or_default())
.unwrap_or_default()
}
}
impl From<SingleSelectTypeOption> for TypeOptionData {
fn from(data: SingleSelectTypeOption) -> Self {
let content = serde_json::to_string(&data).unwrap_or_default();
TypeOptionDataBuilder::new()
.insert_str_value("content", content)
.build()
}
}
impl TypeOptionCellData for SingleSelectTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.get_selected_options(cell_data).into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SelectOptionIds::from(cell))
}
}
impl SelectTypeOptionSharedAction for SingleSelectTypeOption {
fn number_of_max_options(&self) -> Option<usize> {
Some(1)
}
fn to_type_option_data(&self) -> TypeOptionData {
self.clone().into()
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellDataChangeset for SingleSelectTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let mut 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>>();
// In single select, the insert_option_ids should only contain one select option id.
// Sometimes, the insert_option_ids may contain list of option ids. For example,
// copy/paste a ids string.
let select_option_ids = if insert_option_ids.is_empty() {
SelectOptionIds::from(insert_option_ids)
} else {
// Just take the first select option
let _ = insert_option_ids.drain(1..);
SelectOptionIds::from(insert_option_ids)
};
Ok((
select_option_ids.to_cell_data(FieldType::SingleSelect),
select_option_ids,
))
}
}
impl TypeOptionCellDataFilter for SingleSelectTypeOption {
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 SingleSelectTypeOption {
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(),
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataChangeset;
use crate::services::field::type_options::*;
#[test]
fn single_select_transform_with_checkbox_type_option_test() {
let checkbox = CheckboxTypeOption::default();
let mut single_select = SingleSelectTypeOption::default();
single_select.transform_type_option(FieldType::Checkbox, checkbox.clone().into());
debug_assert_eq!(single_select.options.len(), 2);
// Already contain the yes/no option. It doesn't need to insert new options
single_select.transform_type_option(FieldType::Checkbox, checkbox.into());
debug_assert_eq!(single_select.options.len(), 2);
}
#[test]
fn single_select_transform_with_multi_select_type_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let multi_select = MultiSelectTypeOption {
options: vec![google, facebook],
disable_color: false,
};
let mut single_select = SingleSelectTypeOption::default();
single_select.transform_type_option(FieldType::MultiSelect, multi_select.clone().into());
debug_assert_eq!(single_select.options.len(), 2);
// Already contain the yes/no option. It doesn't need to insert new options
single_select.transform_type_option(FieldType::MultiSelect, multi_select.into());
debug_assert_eq!(single_select.options.len(), 2);
}
#[test]
fn single_select_insert_multi_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let single_select = SingleSelectTypeOption {
options: vec![google.clone(), facebook.clone()],
disable_color: false,
};
let option_ids = vec![google.id.clone(), facebook.id];
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids);
let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1;
assert_eq!(&*select_option_ids, &vec![google.id]);
}
#[test]
fn single_select_unselect_multi_option_test() {
let google = SelectOption::new("Google");
let facebook = SelectOption::new("Facebook");
let single_select = SingleSelectTypeOption {
options: vec![google.clone(), facebook.clone()],
disable_color: false,
};
let option_ids = vec![google.id.clone(), facebook.id];
// insert
let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone());
let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1;
assert_eq!(&*select_option_ids, &vec![google.id]);
// 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());
}
}

View File

@ -0,0 +1,64 @@
use crate::entities::FieldType;
use crate::services::field::{
MultiSelectTypeOption, SelectOption, SelectOptionColor, SelectOptionIds,
SelectTypeOptionSharedAction, SingleSelectTypeOption, TypeOption, CHECK, UNCHECK,
};
use collab_database::fields::TypeOptionData;
/// Handles how to transform the cell data when switching between different field types
pub(crate) struct SelectOptionTypeOptionTransformHelper();
impl SelectOptionTypeOptionTransformHelper {
/// Transform the TypeOptionData from 'field_type' to single select option type.
///
/// # Arguments
///
/// * `old_field_type`: the FieldType of the passed-in TypeOptionData
///
pub fn transform_type_option<T>(
shared: &mut T,
old_field_type: &FieldType,
old_type_option_data: TypeOptionData,
) where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds>,
{
match old_field_type {
FieldType::Checkbox => {
//add Yes and No options if it does not exist.
if !shared.options().iter().any(|option| option.name == CHECK) {
let check_option = SelectOption::with_color(CHECK, SelectOptionColor::Green);
shared.mut_options().push(check_option);
}
if !shared.options().iter().any(|option| option.name == UNCHECK) {
let uncheck_option = SelectOption::with_color(UNCHECK, SelectOptionColor::Yellow);
shared.mut_options().push(uncheck_option);
}
},
FieldType::MultiSelect => {
let options = MultiSelectTypeOption::from(old_type_option_data).options;
options.iter().for_each(|new_option| {
if !shared
.options()
.iter()
.any(|option| option.name == new_option.name)
{
shared.mut_options().push(new_option.clone());
}
})
},
FieldType::SingleSelect => {
let options = SingleSelectTypeOption::from(old_type_option_data).options;
options.iter().for_each(|new_option| {
if !shared
.options()
.iter()
.any(|option| option.name == new_option.name)
{
shared.mut_options().push(new_option.clone());
}
})
},
_ => {},
}
}
}

View File

@ -0,0 +1,6 @@
#![allow(clippy::module_inception)]
mod text_filter;
mod text_tests;
mod text_type_option;
pub use text_type_option::*;

View File

@ -0,0 +1,83 @@
use crate::entities::{TextFilterConditionPB, TextFilterPB};
impl TextFilterPB {
pub fn is_visible<T: AsRef<str>>(&self, cell_data: T) -> bool {
let cell_data = cell_data.as_ref().to_lowercase();
let content = &self.content.to_lowercase();
match self.condition {
TextFilterConditionPB::Is => &cell_data == content,
TextFilterConditionPB::IsNot => &cell_data != content,
TextFilterConditionPB::Contains => cell_data.contains(content),
TextFilterConditionPB::DoesNotContain => !cell_data.contains(content),
TextFilterConditionPB::StartsWith => cell_data.starts_with(content),
TextFilterConditionPB::EndsWith => cell_data.ends_with(content),
TextFilterConditionPB::TextIsEmpty => cell_data.is_empty(),
TextFilterConditionPB::TextIsNotEmpty => !cell_data.is_empty(),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]
use crate::entities::{TextFilterConditionPB, TextFilterPB};
#[test]
fn text_filter_equal_test() {
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::Is,
content: "appflowy".to_owned(),
};
assert!(text_filter.is_visible("AppFlowy"));
assert_eq!(text_filter.is_visible("appflowy"), true);
assert_eq!(text_filter.is_visible("Appflowy"), true);
assert_eq!(text_filter.is_visible("AppFlowy.io"), false);
}
#[test]
fn text_filter_start_with_test() {
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::StartsWith,
content: "appflowy".to_owned(),
};
assert_eq!(text_filter.is_visible("AppFlowy.io"), true);
assert_eq!(text_filter.is_visible(""), false);
assert_eq!(text_filter.is_visible("https"), false);
}
#[test]
fn text_filter_end_with_test() {
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::EndsWith,
content: "appflowy".to_owned(),
};
assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true);
assert_eq!(text_filter.is_visible("App"), false);
assert_eq!(text_filter.is_visible("appflowy.io"), false);
}
#[test]
fn text_filter_empty_test() {
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::TextIsEmpty,
content: "appflowy".to_owned(),
};
assert_eq!(text_filter.is_visible(""), true);
assert_eq!(text_filter.is_visible("App"), false);
}
#[test]
fn text_filter_contain_test() {
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::Contains,
content: "appflowy".to_owned(),
};
assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true);
assert_eq!(text_filter.is_visible("AppFlowy"), true);
assert_eq!(text_filter.is_visible("App"), false);
assert_eq!(text_filter.is_visible(""), false);
assert_eq!(text_filter.is_visible("github"), false);
}
}

View File

@ -0,0 +1,97 @@
#[cfg(test)]
mod tests {
use collab_database::rows::Cell;
use crate::entities::FieldType;
use crate::services::cell::stringify_cell_data;
use crate::services::field::FieldBuilder;
use crate::services::field::*;
// Test parser the cell data which field's type is FieldType::Date to cell data
// which field's type is FieldType::Text
#[test]
fn date_type_to_text_type() {
let field_type = FieldType::DateTime;
let field = FieldBuilder::from_field_type(field_type.clone()).build();
assert_eq!(
stringify_cell_data(
&to_text_cell(1647251762.to_string()),
&FieldType::RichText,
&field_type,
&field
),
"Mar 14,2022"
);
let data = DateCellData {
timestamp: Some(1647251762),
include_time: true,
};
assert_eq!(
stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field),
"Mar 14,2022"
);
}
fn to_text_cell(s: String) -> Cell {
StrCellData(s).into()
}
// Test parser the cell data which field's type is FieldType::SingleSelect to cell data
// which field's type is FieldType::Text
#[test]
fn single_select_to_text_type() {
let field_type = FieldType::SingleSelect;
let done_option = SelectOption::new("Done");
let option_id = done_option.id.clone();
let single_select = SingleSelectTypeOption {
options: vec![done_option.clone()],
disable_color: false,
};
let field = FieldBuilder::new(field_type.clone(), single_select).build();
assert_eq!(
stringify_cell_data(
&to_text_cell(option_id),
&FieldType::RichText,
&field_type,
&field
),
done_option.name,
);
}
/*
- [Unit Test] Testing the switching from Multi-selection type to Text type
- Tracking : https://github.com/AppFlowy-IO/AppFlowy/issues/1183
*/
#[test]
fn multiselect_to_text_type() {
let field_type = FieldType::MultiSelect;
let france = SelectOption::new("france");
let france_option_id = france.id.clone();
let argentina = SelectOption::new("argentina");
let argentina_option_id = argentina.id.clone();
let multi_select = MultiSelectTypeOption {
options: vec![france.clone(), argentina.clone()],
disable_color: false,
};
let field_rev = FieldBuilder::new(field_type.clone(), multi_select).build();
assert_eq!(
stringify_cell_data(
&to_text_cell(format!("{},{}", france_option_id, argentina_option_id)),
&FieldType::RichText,
&field_type,
&field_rev
),
format!("{},{}", france.name, argentina.name)
);
}
}

View File

@ -0,0 +1,273 @@
use crate::entities::{FieldType, TextFilterPB};
use crate::services::cell::{
stringify_cell_data, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser, DecodedCellData,
FromCellString,
};
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionTransform, CELL_DATE,
};
use bytes::Bytes;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use crate::services::field::type_options::util::ProtobufStr;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::{FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not
/// used yet.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RichTextTypeOption {
#[serde(default)]
pub inner: String,
}
impl TypeOption for RichTextTypeOption {
type CellData = StrCellData;
type CellChangeset = String;
type CellProtobufType = ProtobufStr;
type CellFilter = TextFilterPB;
}
impl From<TypeOptionData> for RichTextTypeOption {
fn from(data: TypeOptionData) -> Self {
let s = data.get_str_value(CELL_DATE).unwrap_or_default();
Self { inner: s }
}
}
impl From<RichTextTypeOption> for TypeOptionData {
fn from(data: RichTextTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_str_value(CELL_DATE, data.inner)
.build()
}
}
impl TypeOptionTransform for RichTextTypeOption {
fn transformable(&self) -> bool {
true
}
fn transform_type_option(
&mut self,
_old_type_option_field_type: FieldType,
_old_type_option_data: TypeOptionData,
) {
}
fn transform_type_option_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
if _decoded_field_type.is_date()
|| _decoded_field_type.is_single_select()
|| _decoded_field_type.is_multi_select()
|| _decoded_field_type.is_number()
|| _decoded_field_type.is_url()
{
Some(StrCellData::from(stringify_cell_data(
cell,
_decoded_field_type,
_decoded_field_type,
_field,
)))
} else {
Some(StrCellData::from(cell))
}
}
}
impl TypeOptionCellData for RichTextTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
}
}
impl CellDataDecoder for RichTextTypeOption {
fn decode_cell_str(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string()
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
}
impl CellDataChangeset for RichTextTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <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 {
let text_cell_data = StrCellData(changeset);
Ok((text_cell_data.clone().into(), text_cell_data))
}
}
}
impl TypeOptionCellDataFilter for RichTextTypeOption {
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;
}
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for RichTextTypeOption {
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 {
&self.0
}
}
impl std::ops::Deref for TextCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromCellString for TextCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Ok(TextCellData(s.to_owned()))
}
}
impl ToString for TextCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl DecodedCellData for TextCellData {
type Object = TextCellData;
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub struct TextCellDataParser();
impl CellProtobufBlobParser for TextCellDataParser {
type Object = TextCellData;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => Ok(TextCellData(s)),
Err(_) => Ok(TextCellData("".to_owned())),
}
}
}
#[derive(Default, Debug, Clone)]
pub struct StrCellData(pub String);
impl std::ops::Deref for StrCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&Cell> for StrCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value("data").unwrap_or_default())
}
}
impl From<StrCellData> for Cell {
fn from(data: StrCellData) -> Self {
new_cell_builder(FieldType::RichText)
.insert_str_value("data", data.0)
.build()
}
}
impl std::ops::DerefMut for StrCellData {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromCellString for StrCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self> {
Ok(Self(s.to_owned()))
}
}
impl std::convert::From<String> for StrCellData {
fn from(s: String) -> Self {
Self(s)
}
}
impl ToString for StrCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl std::convert::From<StrCellData> for String {
fn from(value: StrCellData) -> Self {
value.0
}
}
impl std::convert::From<&str> for StrCellData {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl AsRef<str> for StrCellData {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}

View File

@ -0,0 +1,230 @@
use std::cmp::Ordering;
use std::fmt::Debug;
use bytes::Bytes;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::Cell;
use protobuf::ProtobufError;
use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
URLTypeOptionPB,
};
use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset};
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, URLTypeOption,
};
use crate::services::filter::FromFilterString;
pub trait TypeOption {
/// `CellData` represents as the decoded model for current type option. Each of them impl the
/// `FromCellString` and `Default` trait. If the cell string can not be decoded into the specified
/// cell data type then the default value will be returned.
/// For example:
/// FieldType::Checkbox => CheckboxCellData
/// FieldType::Date => DateCellData
/// FieldType::URL => URLCellData
///
/// Uses `StrCellData` for any `TypeOption` if their cell data is pure `String`.
///
type CellData: 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.
/// These two traits are auto implemented for `String`.
///
type CellChangeset: FromCellChangeset + ToCellChangeset;
/// For the moment, the protobuf type only be used in the FFI of `Dart`. If the decoded cell
/// struct is just a `String`, then use the `StrCellData` as its `CellProtobufType`.
/// Otherwise, providing a custom protobuf type as its `CellProtobufType`.
/// For example:
/// FieldType::Date => DateCellDataPB
/// 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 {
/// Convert the decoded cell data into corresponding `Protobuf struct`.
/// For example:
/// FieldType::URL => URLCellDataPB
/// FieldType::Date=> DateCellDataPB
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType;
/// Decodes the opaque cell string to corresponding data struct.
// For example, the cell data is timestamp if its field type is `FieldType::Date`. This cell
// data can not directly show to user. So it needs to be encode as the date string with custom
// format setting. Encode `1647251762` to `"Mar 14,2022`
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>;
}
pub trait TypeOptionTransform: TypeOption {
/// Returns true if the current `TypeOption` provides custom type option transformation
fn transformable(&self) -> bool {
false
}
/// Transform the TypeOption from one field type to another
/// For example, when switching from `checkbox` type-option to `single-select`
/// type-option, adding the `Yes` option if the `single-select` type-option doesn't contain it.
/// But the cell content is a string, `Yes`, it's need to do the cell content transform.
/// The `Yes` string will be transformed to the `Yes` option id.
///
/// # Arguments
///
/// * `old_type_option_field_type`: the FieldType of the passed-in TypeOption
/// * `old_type_option_data`: the data that can be parsed into corresponding `TypeOption`.
///
///
fn transform_type_option(
&mut self,
_old_type_option_field_type: FieldType,
_old_type_option_data: TypeOptionData,
) {
}
/// Transform the cell data from one field type to another
///
/// # Arguments
///
/// * `cell_str`: the cell string of the current field type
/// * `decoded_field_type`: the field type of the cell data that's going to be transformed into
/// current `TypeOption` field type.
///
fn transform_type_option_cell(
&self,
_cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> Option<<Self as TypeOption>::CellData> {
None
}
}
pub trait TypeOptionCellDataFilter: TypeOption + CellDataDecoder {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool;
}
#[inline(always)]
pub fn default_order() -> Ordering {
Ordering::Equal
}
pub trait TypeOptionCellDataCompare: TypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering;
}
pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(
bytes: T,
field_type: &FieldType,
) -> TypeOptionData {
let bytes = bytes.into();
let result: Result<TypeOptionData, ProtobufError> = match field_type {
FieldType::RichText => {
RichTextTypeOptionPB::try_from(bytes).map(|pb| RichTextTypeOption::from(pb).into())
},
FieldType::Number => {
NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into())
},
FieldType::DateTime => {
DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into())
},
FieldType::SingleSelect => {
SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into())
},
FieldType::MultiSelect => {
MultiSelectTypeOptionPB::try_from(bytes).map(|pb| MultiSelectTypeOption::from(pb).into())
},
FieldType::Checkbox => {
CheckboxTypeOptionPB::try_from(bytes).map(|pb| CheckboxTypeOption::from(pb).into())
},
FieldType::URL => URLTypeOptionPB::try_from(bytes).map(|pb| URLTypeOption::from(pb).into()),
FieldType::Checklist => {
ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into())
},
};
result.unwrap_or_else(|_| default_type_option_data_for_type(field_type))
}
pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> Bytes {
match field_type {
FieldType::RichText => {
let rich_text_type_option: RichTextTypeOption = type_option.into();
RichTextTypeOptionPB::from(rich_text_type_option)
.try_into()
.unwrap()
},
FieldType::Number => {
let number_type_option: NumberTypeOption = type_option.into();
NumberTypeOptionPB::from(number_type_option)
.try_into()
.unwrap()
},
FieldType::DateTime => {
let date_type_option: DateTypeOption = type_option.into();
DateTypeOptionPB::from(date_type_option).try_into().unwrap()
},
FieldType::SingleSelect => {
let single_select_type_option: SingleSelectTypeOption = type_option.into();
SingleSelectTypeOptionPB::from(single_select_type_option)
.try_into()
.unwrap()
},
FieldType::MultiSelect => {
let multi_select_type_option: MultiSelectTypeOption = type_option.into();
MultiSelectTypeOptionPB::from(multi_select_type_option)
.try_into()
.unwrap()
},
FieldType::Checkbox => {
let checkbox_type_option: CheckboxTypeOption = type_option.into();
CheckboxTypeOptionPB::from(checkbox_type_option)
.try_into()
.unwrap()
},
FieldType::URL => {
let url_type_option: URLTypeOption = type_option.into();
URLTypeOptionPB::from(url_type_option).try_into().unwrap()
},
FieldType::Checklist => {
let checklist_type_option: ChecklistTypeOption = type_option.into();
ChecklistTypeOptionPB::from(checklist_type_option)
.try_into()
.unwrap()
},
}
}
pub fn default_type_option_data_for_type(field_type: &FieldType) -> TypeOptionData {
match field_type {
FieldType::RichText => RichTextTypeOption::default().into(),
FieldType::Number => NumberTypeOption::default().into(),
FieldType::DateTime => DateTypeOption::default().into(),
FieldType::SingleSelect => SingleSelectTypeOption::default().into(),
FieldType::MultiSelect => MultiSelectTypeOption::default().into(),
FieldType::Checkbox => CheckboxTypeOption::default().into(),
FieldType::URL => URLTypeOption::default().into(),
FieldType::Checklist => ChecklistTypeOption::default().into(),
}
}

View File

@ -0,0 +1,570 @@
use std::any::Any;
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, RowId};
use flowy_error::FlowyResult;
use crate::entities::FieldType;
use crate::services::cell::{
CellCache, CellDataChangeset, CellDataDecoder, CellFilterCache, CellProtobufBlob,
FromCellChangeset,
};
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOption,
};
pub const CELL_DATE: &str = "data";
/// 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: Send + Sync + 'static {
fn handle_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field_rev: &Field,
) -> FlowyResult<CellProtobufBlob>;
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_cell: Option<Cell>,
field: &Field,
) -> FlowyResult<Cell>;
fn handle_cell_compare(&self, left_cell: &Cell, right_cell: &Cell, field: &Field) -> Ordering;
fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> 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: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> String;
fn get_cell_data(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<BoxCellData>;
}
struct CellDataCacheKey(u64);
impl CellDataCacheKey {
pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self {
let mut hasher = DefaultHasher::new();
if let Some(type_option_data) = field_rev.get_any_type_option(&decoded_field_type) {
type_option_data.hash(&mut hasher);
}
hasher.write(field_rev.id.as_bytes());
hasher.write_u8(decoded_field_type as u8);
cell.hash(&mut hasher);
Self(hasher.finish())
}
}
impl AsRef<u64> for CellDataCacheKey {
fn as_ref(&self) -> &u64 {
&self.0
}
}
struct TypeOptionCellDataHandlerImpl<T> {
inner: T,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
}
impl<T> TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
+ CellDataDecoder
+ CellDataChangeset
+ TypeOptionCellData
+ TypeOptionTransform
+ TypeOptionCellDataFilter
+ TypeOptionCellDataCompare
+ Send
+ Sync
+ 'static,
{
pub fn new_with_boxed(
inner: T,
cell_filter_cache: Option<CellFilterCache>,
cell_data_cache: Option<CellCache>,
) -> 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 + Send + Sync,
{
fn get_decoded_cell_data(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
let key = CellDataCacheKey::new(field, decoded_field_type.clone(), cell);
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: field_type:{}, cell: {:?}, cell_data: {:?}",
decoded_field_type,
cell,
cell_data
);
return Ok(cell_data);
}
}
let cell_data = self.decode_cell_str(cell, decoded_field_type, field)?;
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
tracing::trace!(
"Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
decoded_field_type,
cell,
cell_data
);
cell_data_cache
.write()
.insert(key.as_ref(), cell_data.clone());
}
Ok(cell_data)
}
fn set_decoded_cell_data(
&self,
cell: &Cell,
cell_data: <Self as TypeOption>::CellData,
field: &Field,
) {
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type = FieldType::from(field.field_type);
let key = CellDataCacheKey::new(field, field_type.clone(), cell);
tracing::trace!(
"Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
field_type,
cell,
cell_data
);
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 + Send + Sync,
{
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
+ Send
+ Sync
+ 'static,
{
fn handle_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field_rev: &Field,
) -> FlowyResult<CellProtobufBlob> {
let cell_data = self
.get_cell_data(cell, decoded_field_type, field_rev)?
.unbox_or_default::<<Self as TypeOption>::CellData>();
CellProtobufBlob::from(self.convert_to_protobuf(cell_data))
}
fn handle_cell_changeset(
&self,
cell_changeset: String,
old_cell: Option<Cell>,
field: &Field,
) -> FlowyResult<Cell> {
let changeset = <Self as TypeOption>::CellChangeset::from_changeset(cell_changeset)?;
let (cell, cell_data) = self.apply_changeset(changeset, old_cell)?;
self.set_decoded_cell_data(&cell, cell_data, field);
Ok(cell)
}
fn handle_cell_compare(&self, left_cell: &Cell, right_cell: &Cell, field: &Field) -> 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();
self.apply_cmp(&left, &right)
}
fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool {
let perform_filter = || {
let filter_cache = self.cell_filter_cache.as_ref()?.read();
let cell_filter = filter_cache.get::<<Self as TypeOption>::CellFilter>(&field.id)?;
let cell_data = self.get_decoded_cell_data(cell, field_type, field).ok()?;
Some(self.apply_filter(cell_filter, field_type, &cell_data))
};
perform_filter().unwrap_or(true)
}
fn stringify_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> String {
if self.transformable() {
let cell_data = self.transform_type_option_cell(cell, decoded_field_type, field);
if let Some(cell_data) = cell_data {
return self.decode_cell_data_to_str(cell_data);
}
}
self.decode_cell_to_str(cell)
}
fn get_cell_data(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<BoxCellData> {
// tracing::debug!("get_cell_data: {:?}", std::any::type_name::<Self>());
let cell_data = if self.transformable() {
match self.transform_type_option_cell(cell, decoded_field_type, field) {
None => self.get_decoded_cell_data(cell, decoded_field_type, field)?,
Some(cell_data) => cell_data,
}
} else {
self.get_decoded_cell_data(cell, decoded_field_type, field)?
};
Ok(BoxCellData::new(cell_data))
}
}
pub struct TypeOptionCellExt<'a> {
field: &'a Field,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
}
impl<'a> TypeOptionCellExt<'a> {
pub fn new_with_cell_data_cache(field: &'a Field, cell_data_cache: Option<CellCache>) -> Self {
Self {
field,
cell_data_cache,
cell_filter_cache: None,
}
}
pub fn new(
field: &'a Field,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
) -> Self {
let mut this = Self::new_with_cell_data_cache(field, cell_data_cache);
this.cell_filter_cache = cell_filter_cache;
this
}
pub fn get_cells<T>(&self) -> Vec<T> {
let field_type = FieldType::from(self.field.field_type);
match self.get_type_option_cell_data_handler(&field_type) {
None => vec![],
Some(_handler) => {
todo!()
},
}
}
pub fn get_type_option_cell_data_handler(
&self,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> {
match field_type {
FieldType::RichText => self
.field
.get_type_option::<RichTextTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::Number => self
.field
.get_type_option::<NumberTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::DateTime => self
.field
.get_type_option::<DateTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::SingleSelect => self
.field
.get_type_option::<SingleSelectTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::MultiSelect => self
.field
.get_type_option::<MultiSelectTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::Checkbox => self
.field
.get_type_option::<CheckboxTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::URL => {
self
.field
.get_type_option::<URLTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
})
},
FieldType::Checklist => self
.field
.get_type_option::<ChecklistTypeOption>(field_type)
.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: &TypeOptionData,
new_field_type: &FieldType,
old_type_option_data: Option<TypeOptionData>,
old_field_type: FieldType,
) -> TypeOptionData {
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.to_type_option_data()
}
/// 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: TypeOptionData,
);
fn to_type_option_data(&self) -> TypeOptionData;
}
impl<T> TypeOptionTransformHandler for T
where
T: TypeOptionTransform + Into<TypeOptionData> + Clone,
{
fn transform(
&mut self,
old_type_option_field_type: FieldType,
old_type_option_data: TypeOptionData,
) {
if self.transformable() {
self.transform_type_option(old_type_option_field_type, old_type_option_data)
}
}
fn to_type_option_data(&self) -> TypeOptionData {
self.clone().into()
}
}
fn get_type_option_transform_handler(
type_option_data: &TypeOptionData,
field_type: &FieldType,
) -> Box<dyn TypeOptionTransformHandler> {
let type_option_data = type_option_data.clone();
match field_type {
FieldType::RichText => {
Box::new(RichTextTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::Number => {
Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::DateTime => {
Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
FieldType::MultiSelect => {
Box::new(MultiSelectTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::Checkbox => {
Box::new(CheckboxTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::URL => {
Box::new(URLTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::Checklist => {
Box::new(ChecklistTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
}
}
pub struct BoxCellData(Box<dyn Any + Send + Sync + 'static>);
impl BoxCellData {
fn new<T>(value: T) -> Self
where
T: Send + Sync + 'static,
{
Self(Box::new(value))
}
fn unbox_or_default<T>(self) -> T
where
T: Default + 'static,
{
match self.0.downcast::<T>() {
Ok(value) => *value,
Err(_) => T::default(),
}
}
pub(crate) fn unbox_or_none<T>(self) -> Option<T>
where
T: Default + 'static,
{
match self.0.downcast::<T>() {
Ok(value) => Some(*value),
Err(_) => None,
}
}
#[allow(dead_code)]
fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.downcast_ref()
}
}
pub struct RowSingleCellData {
pub row_id: RowId,
pub field_id: String,
pub field_type: FieldType,
pub cell_data: BoxCellData,
}
macro_rules! into_cell_data {
($func_name:ident,$return_ty:ty) => {
#[allow(dead_code)]
pub fn $func_name(self) -> Option<$return_ty> {
self.cell_data.unbox_or_none()
}
};
}
impl RowSingleCellData {
into_cell_data!(
into_text_field_cell_data,
<RichTextTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_number_field_cell_data,
<NumberTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_url_field_cell_data,
<URLTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_single_select_field_cell_data,
<SingleSelectTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_multi_select_field_cell_data,
<MultiSelectTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_date_field_cell_data,
<DateTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_check_list_field_cell_data,
<CheckboxTypeOption as TypeOption>::CellData
);
}

View File

@ -0,0 +1,7 @@
#![allow(clippy::module_inception)]
mod url_tests;
mod url_type_option;
mod url_type_option_entities;
pub use url_type_option::*;
pub use url_type_option_entities::*;

View File

@ -0,0 +1,167 @@
#[cfg(test)]
mod tests {
use collab_database::fields::Field;
use crate::entities::FieldType;
use crate::services::cell::CellDataChangeset;
use crate::services::field::FieldBuilder;
use crate::services::field::URLTypeOption;
/// The expected_str will equal to the input string, but the expected_url will be empty if there's no
/// http url in the input string.
#[test]
fn url_type_option_does_not_contain_url_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(&type_option, "123", "123", "", &field);
assert_url(&type_option, "", "", "", &field);
}
/// The expected_str will equal to the input string, but the expected_url will not be empty
/// if there's a http url in the input string.
#[test]
fn url_type_option_contains_url_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(
&type_option,
"AppFlowy website - https://www.appflowy.io",
"AppFlowy website - https://www.appflowy.io",
"https://www.appflowy.io/",
&field,
);
assert_url(
&type_option,
"AppFlowy website appflowy.io",
"AppFlowy website appflowy.io",
"https://appflowy.io",
&field,
);
}
/// if there's a http url and some words following it in the input string.
#[test]
fn url_type_option_contains_url_with_string_after_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(
&type_option,
"AppFlowy website - https://www.appflowy.io welcome!",
"AppFlowy website - https://www.appflowy.io welcome!",
"https://www.appflowy.io/",
&field,
);
assert_url(
&type_option,
"AppFlowy website appflowy.io welcome!",
"AppFlowy website appflowy.io welcome!",
"https://appflowy.io",
&field,
);
}
/// if there's a http url and special words following it in the input string.
#[test]
fn url_type_option_contains_url_with_special_string_after_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(
&type_option,
"AppFlowy website - https://www.appflowy.io!",
"AppFlowy website - https://www.appflowy.io!",
"https://www.appflowy.io/",
&field,
);
assert_url(
&type_option,
"AppFlowy website appflowy.io!",
"AppFlowy website appflowy.io!",
"https://appflowy.io",
&field,
);
}
/// if there's a level4 url in the input string.
#[test]
fn level4_url_type_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(
&type_option,
"test - https://tester.testgroup.appflowy.io",
"test - https://tester.testgroup.appflowy.io",
"https://tester.testgroup.appflowy.io/",
&field,
);
assert_url(
&type_option,
"test tester.testgroup.appflowy.io",
"test tester.testgroup.appflowy.io",
"https://tester.testgroup.appflowy.io",
&field,
);
}
/// urls with different top level domains.
#[test]
fn different_top_level_domains_test() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field = FieldBuilder::from_field_type(field_type).build();
assert_url(
&type_option,
"appflowy - https://appflowy.com",
"appflowy - https://appflowy.com",
"https://appflowy.com/",
&field,
);
assert_url(
&type_option,
"appflowy - https://appflowy.top",
"appflowy - https://appflowy.top",
"https://appflowy.top/",
&field,
);
assert_url(
&type_option,
"appflowy - https://appflowy.net",
"appflowy - https://appflowy.net",
"https://appflowy.net/",
&field,
);
assert_url(
&type_option,
"appflowy - https://appflowy.edu",
"appflowy - https://appflowy.edu",
"https://appflowy.edu/",
&field,
);
}
fn assert_url(
type_option: &URLTypeOption,
input_str: &str,
expected_str: &str,
expected_url: &str,
_field: &Field,
) {
let decode_cell_data = type_option
.apply_changeset(input_str.to_owned(), None)
.unwrap()
.1;
assert_eq!(expected_str.to_owned(), decode_cell_data.data);
assert_eq!(expected_url.to_owned(), decode_cell_data.url);
}
}

View File

@ -0,0 +1,151 @@
use crate::entities::{FieldType, TextFilterPB, URLCellDataPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionTransform, URLCellData,
};
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use fancy_regex::Regex;
use flowy_error::FlowyResult;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct URLTypeOption {
pub url: String,
pub content: String,
}
impl TypeOption for URLTypeOption {
type CellData = URLCellData;
type CellChangeset = URLCellChangeset;
type CellProtobufType = URLCellDataPB;
type CellFilter = TextFilterPB;
}
impl From<TypeOptionData> for URLTypeOption {
fn from(data: TypeOptionData) -> Self {
let url = data.get_str_value("url").unwrap_or_default();
let content = data.get_str_value("content").unwrap_or_default();
Self { url, content }
}
}
impl From<URLTypeOption> for TypeOptionData {
fn from(data: URLTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_str_value("url", data.url)
.insert_str_value("content", data.content)
.build()
}
}
impl TypeOptionTransform for URLTypeOption {}
impl TypeOptionCellData for URLTypeOption {
fn convert_to_protobuf(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
cell_data.into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(URLCellData::from(cell))
}
}
impl CellDataDecoder for URLTypeOption {
fn decode_cell_str(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_url() {
return Ok(Default::default());
}
self.decode_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.data
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
}
}
pub type URLCellChangeset = String;
impl CellDataChangeset for URLTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <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());
}
let url_cell_data = URLCellData {
url,
data: changeset,
};
Ok((url_cell_data.clone().into(), url_cell_data))
}
}
impl TypeOptionCellDataFilter for URLTypeOption {
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 URLTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.data.cmp(&other_cell_data.data)
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(s) {
Ok(url) => {
if url.scheme() == "https" {
url.into()
} else {
format!("https://{}", s)
}
},
Err(_) => {
format!("https://{}", s)
},
}
}
lazy_static! {
static ref URL_REGEX: Regex = Regex::new(
"[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
)
.unwrap();
}

View File

@ -0,0 +1,105 @@
use crate::entities::{FieldType, URLCellDataPB};
use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString};
use crate::services::field::CELL_DATE;
use bytes::Bytes;
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::{internal_error, FlowyResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct URLCellData {
pub url: String,
pub data: String,
}
impl URLCellData {
pub fn new(s: &str) -> Self {
Self {
url: "".to_string(),
data: s.to_string(),
}
}
pub fn to_json(&self) -> FlowyResult<String> {
serde_json::to_string(self).map_err(internal_error)
}
}
impl From<&Cell> for URLCellData {
fn from(cell: &Cell) -> Self {
let url = cell.get_str_value("url").unwrap_or_default();
let content = cell.get_str_value(CELL_DATE).unwrap_or_default();
Self { url, data: content }
}
}
impl From<URLCellData> for Cell {
fn from(data: URLCellData) -> Self {
new_cell_builder(FieldType::URL)
.insert_str_value("url", data.url)
.insert_str_value(CELL_DATE, data.data)
.build()
}
}
impl From<URLCellData> for URLCellDataPB {
fn from(data: URLCellData) -> Self {
Self {
url: data.url,
content: data.data,
}
}
}
impl DecodedCellData for URLCellDataPB {
type Object = URLCellDataPB;
fn is_empty(&self) -> bool {
self.content.is_empty()
}
}
impl From<URLCellDataPB> for URLCellData {
fn from(data: URLCellDataPB) -> Self {
Self {
url: data.url,
data: data.content,
}
}
}
impl AsRef<str> for URLCellData {
fn as_ref(&self) -> &str {
&self.url
}
}
impl DecodedCellData for URLCellData {
type Object = URLCellData;
fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
pub struct URLCellDataParser();
impl CellProtobufBlobParser for URLCellDataParser {
type Object = URLCellDataPB;
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
URLCellDataPB::try_from(bytes.as_ref()).map_err(internal_error)
}
}
impl FromCellString for URLCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self> {
serde_json::from_str::<URLCellData>(s).map_err(internal_error)
}
}
impl ToString for URLCellData {
fn to_string(&self) -> String {
self.to_json().unwrap()
}
}

View File

@ -0,0 +1,49 @@
use bytes::Bytes;
use protobuf::ProtobufError;
#[derive(Default, Debug, Clone)]
pub struct ProtobufStr(pub String);
impl std::ops::Deref for ProtobufStr {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ProtobufStr {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::convert::From<String> for ProtobufStr {
fn from(s: String) -> Self {
Self(s)
}
}
impl ToString for ProtobufStr {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl std::convert::TryFrom<ProtobufStr> for Bytes {
type Error = ProtobufError;
fn try_from(value: ProtobufStr) -> Result<Self, Self::Error> {
Ok(Bytes::from(value.0))
}
}
impl AsRef<[u8]> for ProtobufStr {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl AsRef<str> for ProtobufStr {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}

View File

@ -0,0 +1,443 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::rows::{Cell, Row, RowId};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use flowy_error::FlowyResult;
use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher};
use lib_infra::future::Fut;
use crate::entities::filter_entities::*;
use crate::entities::{FieldType, InsertedRowPB, RowPB};
use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache};
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
use crate::services::field::*;
use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification};
pub trait FilterDelegate: Send + Sync + 'static {
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>>;
fn get_field(&self, field_id: &str) -> Fut<Option<Arc<Field>>>;
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>>;
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>>;
fn get_row(&self, view_id: &str, rows_id: RowId) -> Fut<Option<(usize, Arc<Row>)>>;
}
pub trait FromFilterString {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized;
}
pub struct FilterController {
view_id: String,
handler_id: String,
delegate: Box<dyn FilterDelegate>,
result_by_row_id: DashMap<RowId, FilterResult>,
cell_cache: CellCache,
cell_filter_cache: CellFilterCache,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
notifier: DatabaseViewChangedNotifier,
}
impl Drop for FilterController {
fn drop(&mut self) {
tracing::trace!("Drop {}", std::any::type_name::<Self>());
}
}
impl FilterController {
pub async fn new<T>(
view_id: &str,
handler_id: &str,
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
filters: Vec<Arc<Filter>>,
cell_cache: CellCache,
notifier: DatabaseViewChangedNotifier,
) -> Self
where
T: FilterDelegate + 'static,
{
let this = Self {
view_id: view_id.to_string(),
handler_id: handler_id.to_string(),
delegate: Box::new(delegate),
result_by_row_id: DashMap::default(),
cell_cache,
// Cache by field_id
cell_filter_cache: AnyTypeCache::<String>::new(),
task_scheduler,
notifier,
};
this.refresh_filters(filters).await;
this
}
pub async fn close(&self) {
if let Ok(mut task_scheduler) = self.task_scheduler.try_write() {
task_scheduler.unregister_handler(&self.handler_id).await;
} else {
tracing::error!("Try to get the lock of task_scheduler failed");
}
}
#[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))]
async fn gen_task(&self, task_type: FilterEvent, qos: QualityOfService) {
let task_id = self.task_scheduler.read().await.next_task_id();
let task = Task::new(
&self.handler_id,
task_id,
TaskContent::Text(task_type.to_string()),
qos,
);
self.task_scheduler.write().await.add_task(task);
}
pub async fn filter_rows(&self, rows: &mut Vec<Arc<Row>>) {
if self.cell_filter_cache.read().is_empty() {
return;
}
let field_by_field_id = self.get_field_map().await;
rows.iter().for_each(|row| {
let _ = filter_row(
row,
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
);
});
rows.retain(|row| {
self
.result_by_row_id
.get(&row.id)
.map(|result| result.is_visible())
.unwrap_or(false)
});
}
async fn get_field_map(&self) -> HashMap<String, Arc<Field>> {
self
.delegate
.get_fields(&self.view_id, None)
.await
.into_iter()
.map(|field| (field.id.clone(), field))
.collect::<HashMap<String, Arc<Field>>>()
}
#[tracing::instrument(
name = "process_filter_task",
level = "trace",
skip_all,
fields(filter_result),
err
)]
pub async fn process(&self, predicate: &str) -> FlowyResult<()> {
let event_type = FilterEvent::from_str(predicate).unwrap();
match event_type {
FilterEvent::FilterDidChanged => self.filter_all_rows().await?,
FilterEvent::RowDidChanged(row_id) => self.filter_row(row_id).await?,
}
Ok(())
}
async fn filter_row(&self, row_id: RowId) -> FlowyResult<()> {
if let Some((_, row)) = self.delegate.get_row(&self.view_id, row_id).await {
let field_by_field_id = self.get_field_map().await;
let mut notification = FilterResultNotification::new(self.view_id.clone());
if let Some((row_id, is_visible)) = filter_row(
&row,
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
) {
if is_visible {
if let Some((index, row)) = self.delegate.get_row(&self.view_id, row_id).await {
let row_pb = RowPB::from(row.as_ref());
notification
.visible_rows
.push(InsertedRowPB::with_index(row_pb, index as i32))
}
} else {
notification.invisible_rows.push(row_id.into());
}
}
let _ = self
.notifier
.send(DatabaseViewChanged::FilterNotification(notification));
}
Ok(())
}
async fn filter_all_rows(&self) -> FlowyResult<()> {
let field_by_field_id = self.get_field_map().await;
let mut visible_rows = vec![];
let mut invisible_rows = vec![];
for (index, row) in self
.delegate
.get_rows(&self.view_id)
.await
.into_iter()
.enumerate()
{
if let Some((row_id, is_visible)) = filter_row(
&row,
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
) {
if is_visible {
let row_pb = RowPB::from(row.as_ref());
visible_rows.push(InsertedRowPB::with_index(row_pb, index as i32))
} else {
invisible_rows.push(i64::from(row_id));
}
}
}
let notification = FilterResultNotification {
view_id: self.view_id.clone(),
invisible_rows,
visible_rows,
};
tracing::Span::current().record("filter_result", format!("{:?}", &notification).as_str());
let _ = self
.notifier
.send(DatabaseViewChanged::FilterNotification(notification));
Ok(())
}
pub async fn did_receive_row_changed(&self, row_id: RowId) {
self
.gen_task(
FilterEvent::RowDidChanged(row_id),
QualityOfService::UserInteractive,
)
.await
}
#[tracing::instrument(level = "trace", skip(self))]
pub async fn did_receive_changes(
&self,
changeset: FilterChangeset,
) -> Option<FilterChangesetNotificationPB> {
let mut notification: Option<FilterChangesetNotificationPB> = None;
if let Some(filter_type) = &changeset.insert_filter {
if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await {
notification = Some(FilterChangesetNotificationPB::from_insert(
&self.view_id,
vec![filter],
));
}
if let Some(filter) = self
.delegate
.get_filter(&self.view_id, &filter_type.filter_id)
.await
{
self.refresh_filters(vec![filter]).await;
}
}
if let Some(updated_filter_type) = changeset.update_filter {
if let Some(old_filter_type) = updated_filter_type.old {
let new_filter = self
.filter_from_filter_id(&updated_filter_type.new.filter_id)
.await;
let old_filter = self.filter_from_filter_id(&old_filter_type.filter_id).await;
// Get the filter id
let mut filter_id = old_filter.map(|filter| filter.id);
if filter_id.is_none() {
filter_id = new_filter.as_ref().map(|filter| filter.id.clone());
}
if let Some(filter_id) = filter_id {
// Update the corresponding filter in the cache
if let Some(filter) = self.delegate.get_filter(&self.view_id, &filter_id).await {
self.refresh_filters(vec![filter]).await;
}
notification = Some(FilterChangesetNotificationPB::from_update(
&self.view_id,
vec![UpdatedFilter {
filter_id,
filter: new_filter,
}],
));
}
}
}
if let Some(filter_type) = &changeset.delete_filter {
if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await {
notification = Some(FilterChangesetNotificationPB::from_delete(
&self.view_id,
vec![filter],
));
}
self.cell_filter_cache.write().remove(&filter_type.field_id);
}
self
.gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background)
.await;
tracing::trace!("{:?}", notification);
notification
}
async fn filter_from_filter_id(&self, filter_id: &str) -> Option<FilterPB> {
self
.delegate
.get_filter(&self.view_id, filter_id)
.await
.map(|filter| FilterPB::from(filter.as_ref()))
}
#[tracing::instrument(level = "trace", skip_all)]
async fn refresh_filters(&self, filters: Vec<Arc<Filter>>) {
for filter in filters {
let field_id = &filter.field_id;
tracing::trace!("Create filter with type: {:?}", filter.field_type);
match &filter.field_type {
FieldType::RichText => {
self
.cell_filter_cache
.write()
.insert(field_id, TextFilterPB::from_filter(filter.as_ref()));
},
FieldType::Number => {
self
.cell_filter_cache
.write()
.insert(field_id, NumberFilterPB::from_filter(filter.as_ref()));
},
FieldType::DateTime => {
self
.cell_filter_cache
.write()
.insert(field_id, DateFilterPB::from_filter(filter.as_ref()));
},
FieldType::SingleSelect | FieldType::MultiSelect => {
self
.cell_filter_cache
.write()
.insert(field_id, SelectOptionFilterPB::from_filter(filter.as_ref()));
},
FieldType::Checkbox => {
self
.cell_filter_cache
.write()
.insert(field_id, CheckboxFilterPB::from_filter(filter.as_ref()));
},
FieldType::URL => {
self
.cell_filter_cache
.write()
.insert(field_id, TextFilterPB::from_filter(filter.as_ref()));
},
FieldType::Checklist => {
self
.cell_filter_cache
.write()
.insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref()));
},
}
}
}
}
/// Returns None if there is no change in this row after applying the filter
#[tracing::instrument(level = "trace", skip_all)]
fn filter_row(
row: &Row,
result_by_row_id: &DashMap<RowId, FilterResult>,
field_by_field_id: &HashMap<String, Arc<Field>>,
cell_data_cache: &CellCache,
cell_filter_cache: &CellFilterCache,
) -> Option<(RowId, bool)> {
// Create a filter result cache if it's not exist
let mut filter_result = result_by_row_id
.entry(row.id)
.or_insert_with(FilterResult::default);
let old_is_visible = filter_result.is_visible();
// Iterate each cell of the row to check its visibility
for (field_id, field) in field_by_field_id {
if !cell_filter_cache.read().contains(field_id) {
filter_result.visible_by_field_id.remove(field_id);
continue;
}
let cell = row.cells.get(field_id).cloned();
let field_type = FieldType::from(field.field_type);
// 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(&field_type, field, cell, cell_data_cache, cell_filter_cache)
{
filter_result
.visible_by_field_id
.insert(field_id.to_string(), is_visible);
}
}
let is_visible = filter_result.is_visible();
if old_is_visible != is_visible {
Some((row.id, is_visible))
} else {
None
}
}
// Returns None if there is no change in this cell after applying the filter
// Returns Some if the visibility of the cell is changed
#[tracing::instrument(level = "trace", skip_all, fields(cell_content))]
fn filter_cell(
field_type: &FieldType,
field: &Arc<Field>,
cell: Option<Cell>,
cell_data_cache: &CellCache,
cell_filter_cache: &CellFilterCache,
) -> Option<bool> {
let handler = TypeOptionCellExt::new(
field.as_ref(),
Some(cell_data_cache.clone()),
Some(cell_filter_cache.clone()),
)
.get_type_option_cell_data_handler(field_type)?;
let is_visible =
handler.handle_cell_filter(field_type, field.as_ref(), &cell.unwrap_or_default());
Some(is_visible)
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum FilterEvent {
FilterDidChanged,
RowDidChanged(RowId),
}
impl ToString for FilterEvent {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
impl FromStr for FilterEvent {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}

View File

@ -0,0 +1,175 @@
use anyhow::bail;
use collab::core::any_map::AnyMapExtension;
use collab_database::views::{FilterMap, FilterMapBuilder};
use crate::entities::{DeleteFilterParams, FieldType, FilterPB, InsertedRowPB};
#[derive(Debug, Clone)]
pub struct Filter {
pub id: String,
pub field_id: String,
pub field_type: FieldType,
pub condition: i64,
pub content: String,
}
const FILTER_ID: &str = "id";
const FIELD_ID: &str = "field_id";
const FIELD_TYPE: &str = "ty";
const FILTER_CONDITION: &str = "condition";
const FILTER_CONTENT: &str = "content";
impl From<Filter> for FilterMap {
fn from(data: Filter) -> Self {
FilterMapBuilder::new()
.insert_str_value(FILTER_ID, data.id)
.insert_str_value(FIELD_ID, data.field_id)
.insert_str_value(FILTER_CONTENT, data.content)
.insert_i64_value(FIELD_TYPE, data.field_type.into())
.insert_i64_value(FILTER_CONDITION, data.condition)
.build()
}
}
impl TryFrom<FilterMap> for Filter {
type Error = anyhow::Error;
fn try_from(filter: FilterMap) -> Result<Self, Self::Error> {
match (
filter.get_str_value(FILTER_ID),
filter.get_str_value(FIELD_ID),
) {
(Some(id), Some(field_id)) => {
let condition = filter.get_i64_value(FILTER_CONDITION).unwrap_or(0);
let content = filter.get_str_value(FILTER_CONTENT).unwrap_or_default();
let field_type = filter
.get_i64_value(FIELD_TYPE)
.map(FieldType::from)
.unwrap_or_default();
Ok(Filter {
id,
field_id,
field_type,
condition,
content,
})
},
_ => {
bail!("Invalid filter data")
},
}
}
}
#[derive(Debug)]
pub struct FilterChangeset {
pub(crate) insert_filter: Option<FilterType>,
pub(crate) update_filter: Option<UpdatedFilterType>,
pub(crate) delete_filter: Option<FilterType>,
}
#[derive(Debug)]
pub struct UpdatedFilterType {
pub old: Option<FilterType>,
pub new: FilterType,
}
impl UpdatedFilterType {
pub fn new(old: Option<FilterType>, new: FilterType) -> UpdatedFilterType {
Self { old, new }
}
}
impl FilterChangeset {
pub fn from_insert(filter_type: FilterType) -> Self {
Self {
insert_filter: Some(filter_type),
update_filter: None,
delete_filter: None,
}
}
pub fn from_update(filter_type: UpdatedFilterType) -> Self {
Self {
insert_filter: None,
update_filter: Some(filter_type),
delete_filter: None,
}
}
pub fn from_delete(filter_type: FilterType) -> Self {
Self {
insert_filter: None,
update_filter: None,
delete_filter: Some(filter_type),
}
}
}
#[derive(Hash, Eq, PartialEq, Debug, Clone)]
pub struct FilterType {
pub filter_id: String,
pub field_id: String,
pub field_type: FieldType,
}
impl std::convert::From<&Filter> for FilterType {
fn from(filter: &Filter) -> Self {
Self {
filter_id: filter.id.clone(),
field_id: filter.field_id.clone(),
field_type: filter.field_type.clone(),
}
}
}
impl std::convert::From<&FilterPB> for FilterType {
fn from(filter: &FilterPB) -> Self {
Self {
filter_id: filter.id.clone(),
field_id: filter.field_id.clone(),
field_type: filter.field_type.clone(),
}
}
}
// #[derive(Hash, Eq, PartialEq, Debug, Clone)]
// pub struct InsertedFilterType {
// pub field_id: String,
// pub filter_id: Option<String>,
// pub field_type: FieldType,
// }
//
// impl std::convert::From<&Filter> for InsertedFilterType {
// fn from(params: &Filter) -> Self {
// Self {
// field_id: params.field_id.clone(),
// filter_id: Some(params.id.clone()),
// field_type: params.field_type.clone(),
// }
// }
// }
impl std::convert::From<&DeleteFilterParams> for FilterType {
fn from(params: &DeleteFilterParams) -> Self {
params.filter_type.clone()
}
}
#[derive(Clone, Debug)]
pub struct FilterResultNotification {
pub view_id: String,
// Indicates there will be some new rows being visible from invisible state.
pub visible_rows: Vec<InsertedRowPB>,
// Indicates there will be some new rows being invisible from visible state.
pub invisible_rows: Vec<i64>,
}
impl FilterResultNotification {
pub fn new(view_id: String) -> Self {
Self {
view_id,
visible_rows: vec![],
invisible_rows: vec![],
}
}
}

View File

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

View File

@ -0,0 +1,60 @@
use crate::services::filter::FilterController;
use flowy_task::{TaskContent, TaskHandler};
use lib_infra::future::BoxResultFuture;
use std::collections::HashMap;
use std::sync::Arc;
pub struct FilterTaskHandler {
handler_id: String,
filter_controller: Arc<FilterController>,
}
impl FilterTaskHandler {
pub fn new(handler_id: String, filter_controller: Arc<FilterController>) -> Self {
Self {
handler_id,
filter_controller,
}
}
}
impl TaskHandler for FilterTaskHandler {
fn handler_id(&self) -> &str {
&self.handler_id
}
fn handler_name(&self) -> &str {
"FilterTaskHandler"
}
fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> {
let filter_controller = self.filter_controller.clone();
Box::pin(async move {
if let TaskContent::Text(predicate) = content {
filter_controller
.process(&predicate)
.await
.map_err(anyhow::Error::from)?;
}
Ok(())
})
}
}
/// Refresh the filter according to the field id.
#[derive(Default)]
pub(crate) struct FilterResult {
pub(crate) visible_by_field_id: HashMap<String, bool>,
}
impl FilterResult {
pub(crate) fn is_visible(&self) -> bool {
let mut is_visible = true;
for visible in self.visible_by_field_id.values() {
if !is_visible {
break;
}
is_visible = *visible;
}
is_visible
}
}

View File

@ -0,0 +1,117 @@
use crate::entities::{GroupChangesetPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
use crate::services::cell::DecodedCellData;
use crate::services::group::controller::MoveGroupRowContext;
use crate::services::group::GroupData;
use collab_database::fields::Field;
use collab_database::rows::{Cell, Row};
use flowy_error::FlowyResult;
/// Using polymorphism to provides the customs action for different group controller.
///
/// For example, the `CheckboxGroupController` implements this trait to provide custom behavior.
///
pub trait GroupCustomize: Send + Sync {
type CellData: DecodedCellData;
/// Returns the a value of the cell if the cell data is not exist.
/// The default value is `None`
///
/// Determine which group the row is placed in based on the data of the cell. If the cell data
/// is None. The row will be put in to the `No status` group
///
fn placeholder_cell(&self) -> Option<Cell> {
None
}
/// Returns a bool value to determine whether the group should contain this cell or not.
fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool;
fn create_or_delete_group_when_cell_changed(
&mut self,
_row: &Row,
_old_cell_data: Option<&Self::CellData>,
_cell_data: &Self::CellData,
) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> {
Ok((None, None))
}
/// Adds or removes a row if the cell data match the group filter.
/// It gets called after editing the cell or row
///
fn add_or_remove_row_when_cell_changed(
&mut self,
row: &Row,
cell_data: &Self::CellData,
) -> Vec<GroupRowsNotificationPB>;
/// Deletes the row from the group
fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB>;
/// Move row from one group to another
fn move_row(
&mut self,
cell_data: &Self::CellData,
context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB>;
/// Returns None if there is no need to delete the group when corresponding row get removed
fn delete_group_when_move_row(
&mut self,
_row: &Row,
_cell_data: &Self::CellData,
) -> Option<GroupPB> {
None
}
}
/// Defines the shared actions any group controller can perform.
pub trait GroupControllerActions: Send + Sync {
/// The field that is used for grouping the rows
fn field_id(&self) -> &str;
/// Returns number of groups the current field has
fn groups(&self) -> Vec<&GroupData>;
/// Returns the index and the group data with group_id
fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>;
/// Separates the rows into different groups
fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()>;
/// Remove the group with from_group_id and insert it to the index with to_group_id
fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>;
/// Insert/Remove the row to the group if the corresponding cell data is changed
fn did_update_group_row(
&mut self,
old_row: &Option<Row>,
row: &Row,
field: &Field,
) -> FlowyResult<DidUpdateGroupRowResult>;
/// Remove the row from the group if the row gets deleted
fn did_delete_delete_row(
&mut self,
row: &Row,
field: &Field,
) -> FlowyResult<DidMoveGroupRowResult>;
/// Move the row from one group to another group
fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult>;
/// Update the group if the corresponding field is changed
fn did_update_group_field(&mut self, field: &Field) -> FlowyResult<Option<GroupChangesetPB>>;
}
#[derive(Debug)]
pub struct DidUpdateGroupRowResult {
pub(crate) inserted_group: Option<InsertedGroupPB>,
pub(crate) deleted_group: Option<GroupPB>,
pub(crate) row_changesets: Vec<GroupRowsNotificationPB>,
}
#[derive(Debug)]
pub struct DidMoveGroupRowResult {
pub(crate) deleted_group: Option<GroupPB>,
pub(crate) row_changesets: Vec<GroupRowsNotificationPB>,
}

View File

@ -0,0 +1,481 @@
use crate::entities::{GroupChangesetPB, GroupPB, InsertedGroupPB};
use crate::services::field::RowSingleCellData;
use crate::services::group::{
default_group_setting, GeneratedGroupContext, Group, GroupData, GroupSetting,
};
use collab_database::fields::Field;
use flowy_error::{FlowyError, FlowyResult};
use indexmap::IndexMap;
use lib_infra::future::Fut;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::sync::Arc;
pub trait GroupSettingReader: Send + Sync + 'static {
fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>>;
fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>>;
}
pub trait GroupSettingWriter: Send + Sync + 'static {
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>>;
}
impl<T> std::fmt::Display for GroupContext<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.groups_map.iter().for_each(|(_, group)| {
let _ = f.write_fmt(format_args!(
"Group:{} has {} rows \n",
group.id,
group.rows.len()
));
});
Ok(())
}
}
/// A [GroupContext] represents as the groups memory cache
/// Each [GenericGroupController] has its own [GroupContext], the `context` has its own configuration
/// that is restored from the disk.
///
/// The `context` contains a list of [GroupData]s and the grouping [Field]
pub struct GroupContext<C> {
pub view_id: String,
/// The group configuration restored from the disk.
///
/// Uses the [GroupSettingReader] to read the configuration data from disk
setting: Arc<GroupSetting>,
configuration_phantom: PhantomData<C>,
/// The grouping field
field: Arc<Field>,
/// Cache all the groups
groups_map: IndexMap<String, GroupData>,
/// A reader that implement the [GroupSettingReader] trait
///
#[allow(dead_code)]
reader: Arc<dyn GroupSettingReader>,
/// A writer that implement the [GroupSettingWriter] trait is used to save the
/// configuration to disk
///
writer: Arc<dyn GroupSettingWriter>,
}
impl<C> GroupContext<C>
where
C: Serialize + DeserializeOwned,
{
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn new(
view_id: String,
field: Arc<Field>,
reader: Arc<dyn GroupSettingReader>,
writer: Arc<dyn GroupSettingWriter>,
) -> FlowyResult<Self> {
let setting = match reader.get_group_setting(&view_id).await {
None => {
let default_configuration = default_group_setting(&field);
writer
.save_configuration(&view_id, default_configuration.clone())
.await?;
Arc::new(default_configuration)
},
Some(setting) => setting,
};
Ok(Self {
view_id,
field,
groups_map: IndexMap::new(),
reader,
writer,
setting,
configuration_phantom: PhantomData,
})
}
/// Returns the no `status` group
///
/// We take the `id` of the `field` as the no status group id
pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> {
self.groups_map.get(&self.field.id)
}
pub(crate) fn get_mut_no_status_group(&mut self) -> Option<&mut GroupData> {
self.groups_map.get_mut(&self.field.id)
}
pub(crate) fn groups(&self) -> Vec<&GroupData> {
self.groups_map.values().collect()
}
pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut GroupData> {
self.groups_map.get_mut(group_id)
}
// Returns the index and group specified by the group_id
pub(crate) fn get_group(&self, group_id: &str) -> Option<(usize, &GroupData)> {
match (
self.groups_map.get_index_of(group_id),
self.groups_map.get(group_id),
) {
(Some(index), Some(group)) => Some((index, group)),
_ => None,
}
}
/// Iterate mut the groups without `No status` group
pub(crate) fn iter_mut_status_groups(&mut self, mut each: impl FnMut(&mut GroupData)) {
self.groups_map.iter_mut().for_each(|(_, group)| {
if group.id != self.field.id {
each(group);
}
});
}
pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut GroupData)) {
self.groups_map.iter_mut().for_each(|(_, group)| {
each(group);
});
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) fn add_new_group(&mut self, group: Group) -> FlowyResult<InsertedGroupPB> {
let group_data = GroupData::new(
group.id.clone(),
self.field.id.clone(),
group.name.clone(),
group.id.clone(),
);
self.groups_map.insert(group.id.clone(), group_data);
let (index, group_data) = self.get_group(&group.id).unwrap();
let insert_group = InsertedGroupPB {
group: GroupPB::from(group_data.clone()),
index: index as i32,
};
self.mut_configuration(|configuration| {
configuration.groups.push(group);
true
})?;
Ok(insert_group)
}
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) fn delete_group(&mut self, deleted_group_id: &str) -> FlowyResult<()> {
self.groups_map.remove(deleted_group_id);
self.mut_configuration(|configuration| {
configuration
.groups
.retain(|group| group.id != deleted_group_id);
true
})?;
Ok(())
}
pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> {
let from_index = self.groups_map.get_index_of(from_id);
let to_index = self.groups_map.get_index_of(to_id);
match (from_index, to_index) {
(Some(from_index), Some(to_index)) => {
self.groups_map.move_index(from_index, to_index);
self.mut_configuration(|configuration| {
let from_index = configuration
.groups
.iter()
.position(|group| group.id == from_id);
let to_index = configuration
.groups
.iter()
.position(|group| group.id == to_id);
if let (Some(from), Some(to)) = &(from_index, to_index) {
tracing::trace!(
"Move group from index:{:?} to index:{:?}",
from_index,
to_index
);
let group = configuration.groups.remove(*from);
configuration.groups.insert(*to, group);
}
tracing::debug!(
"Group order: {:?} ",
configuration
.groups
.iter()
.map(|group| group.name.clone())
.collect::<Vec<String>>()
.join(",")
);
from_index.is_some() && to_index.is_some()
})?;
Ok(())
},
_ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")),
}
}
/// Reset the memory cache of the groups and update the group configuration
///
/// # Arguments
///
/// * `generated_group_configs`: the generated groups contains a list of [GeneratedGroupConfig].
///
/// Each [FieldType] can implement the [GroupGenerator] trait in order to generate different
/// groups. For example, the FieldType::Checkbox has the [CheckboxGroupGenerator] that implements
/// the [GroupGenerator] trait.
///
/// Consider the passed-in generated_group_configs as new groups, the groups in the current
/// [GroupConfigurationRevision] as old groups. The old groups and the new groups will be merged
/// while keeping the order of the old groups.
///
#[tracing::instrument(level = "trace", skip(self, generated_group_context), err)]
pub(crate) fn init_groups(
&mut self,
generated_group_context: GeneratedGroupContext,
) -> FlowyResult<Option<GroupChangesetPB>> {
let GeneratedGroupContext {
no_status_group,
group_configs,
} = generated_group_context;
let mut new_groups = vec![];
let mut filter_content_map = HashMap::new();
group_configs.into_iter().for_each(|generate_group| {
filter_content_map.insert(
generate_group.group.id.clone(),
generate_group.filter_content,
);
new_groups.push(generate_group.group);
});
let mut old_groups = self.setting.groups.clone();
// clear all the groups if grouping by a new field
if self.setting.field_id != self.field.id {
old_groups.clear();
}
// The `all_group_revs` is the combination of the new groups and old groups
let MergeGroupResult {
mut all_groups,
new_groups,
deleted_groups,
} = merge_groups(no_status_group, old_groups, new_groups);
let deleted_group_ids = deleted_groups
.into_iter()
.map(|group_rev| group_rev.id)
.collect::<Vec<String>>();
self.mut_configuration(|configuration| {
let mut is_changed = !deleted_group_ids.is_empty();
// Remove the groups
configuration
.groups
.retain(|group| !deleted_group_ids.contains(&group.id));
// Update/Insert new groups
for group in &mut all_groups {
match configuration
.groups
.iter()
.position(|old_group_rev| old_group_rev.id == group.id)
{
None => {
// Push the group to the end of the list if it doesn't exist in the group
configuration.groups.push(group.clone());
is_changed = true;
},
Some(pos) => {
let mut old_group = configuration.groups.get_mut(pos).unwrap();
// Take the old group setting
group.visible = old_group.visible;
if !is_changed {
is_changed = is_group_changed(group, old_group);
}
// Consider the the name of the `group_rev` as the newest.
old_group.name = group.name.clone();
},
}
}
is_changed
})?;
// Update the memory cache of the groups
all_groups.into_iter().for_each(|group_rev| {
let filter_content = filter_content_map
.get(&group_rev.id)
.cloned()
.unwrap_or_else(|| "".to_owned());
let group = GroupData::new(
group_rev.id,
self.field.id.clone(),
group_rev.name,
filter_content,
);
self.groups_map.insert(group.id.clone(), group);
});
let initial_groups = new_groups
.into_iter()
.flat_map(|group_rev| {
let filter_content = filter_content_map.get(&group_rev.id)?;
let group = GroupData::new(
group_rev.id,
self.field.id.clone(),
group_rev.name,
filter_content.clone(),
);
Some(GroupPB::from(group))
})
.collect();
let changeset = GroupChangesetPB {
view_id: self.view_id.clone(),
initial_groups,
deleted_groups: deleted_group_ids,
update_groups: vec![],
inserted_groups: vec![],
};
tracing::trace!("Group changeset: {:?}", changeset);
if changeset.is_empty() {
Ok(None)
} else {
Ok(Some(changeset))
}
}
#[allow(dead_code)]
pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> {
self.mut_group_rev(group_id, |group_rev| {
group_rev.visible = false;
})?;
Ok(())
}
#[allow(dead_code)]
pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> {
self.mut_group_rev(group_id, |group_rev| {
group_rev.visible = true;
})?;
Ok(())
}
pub(crate) async fn get_all_cells(&self) -> Vec<RowSingleCellData> {
self
.reader
.get_configuration_cells(&self.view_id, &self.field.id)
.await
}
fn mut_configuration(
&mut self,
mut_configuration_fn: impl FnOnce(&mut GroupSetting) -> bool,
) -> FlowyResult<()> {
let configuration = Arc::make_mut(&mut self.setting);
let is_changed = mut_configuration_fn(configuration);
if is_changed {
let configuration = (*self.setting).clone();
let writer = self.writer.clone();
let field_id = self.field.id.clone();
tokio::spawn(async move {
match writer.save_configuration(&field_id, configuration).await {
Ok(_) => {},
Err(e) => {
tracing::error!("Save group configuration failed: {}", e);
},
}
});
}
Ok(())
}
fn mut_group_rev(
&mut self,
group_id: &str,
mut_groups_fn: impl Fn(&mut Group),
) -> FlowyResult<()> {
self.mut_configuration(|configuration| {
match configuration
.groups
.iter_mut()
.find(|group| group.id == group_id)
{
None => false,
Some(group) => {
mut_groups_fn(group);
true
},
}
})
}
}
/// Merge the new groups into old groups while keeping the order in the old groups
///
fn merge_groups(
no_status_group: Option<Group>,
old_groups: Vec<Group>,
new_groups: Vec<Group>,
) -> MergeGroupResult {
let mut merge_result = MergeGroupResult::new();
// group_map is a helper map is used to filter out the new groups.
let mut new_group_map: IndexMap<String, Group> = IndexMap::new();
new_groups.into_iter().for_each(|group_rev| {
new_group_map.insert(group_rev.id.clone(), group_rev);
});
// The group is ordered in old groups. Add them before adding the new groups
for old in old_groups {
if let Some(new) = new_group_map.remove(&old.id) {
merge_result.all_groups.push(new.clone());
} else {
merge_result.deleted_groups.push(old);
}
}
// Find out the new groups
let new_groups = new_group_map.into_values();
for (_, group) in new_groups.into_iter().enumerate() {
merge_result.all_groups.push(group.clone());
merge_result.new_groups.push(group);
}
// The `No status` group index is initialized to 0
if let Some(no_status_group) = no_status_group {
merge_result.all_groups.insert(0, no_status_group);
}
merge_result
}
fn is_group_changed(new: &Group, old: &Group) -> bool {
if new.name != old.name {
return true;
}
false
}
struct MergeGroupResult {
// Contains the new groups and the updated groups
all_groups: Vec<Group>,
new_groups: Vec<Group>,
deleted_groups: Vec<Group>,
}
impl MergeGroupResult {
fn new() -> Self {
Self {
all_groups: vec![],
new_groups: vec![],
deleted_groups: vec![],
}
}
}

View File

@ -0,0 +1,382 @@
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Cells, Row, RowId};
use serde::de::DeserializeOwned;
use serde::Serialize;
use flowy_error::FlowyResult;
use crate::entities::{FieldType, GroupChangesetPB, GroupRowsNotificationPB, InsertedRowPB};
use crate::services::cell::{get_type_cell_protobuf, CellProtobufBlobParser, DecodedCellData};
use crate::services::group::action::{
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions, GroupCustomize,
};
use crate::services::group::configuration::GroupContext;
use crate::services::group::entities::GroupData;
use crate::services::group::Group;
// use collab_database::views::Group;
/// The [GroupController] trait defines the group actions, including create/delete/move items
/// For example, the group will insert a item if the one of the new [RowRevision]'s [CellRevision]s
/// content match the group filter.
///
/// Different [FieldType] has a different controller that implements the [GroupController] trait.
/// If the [FieldType] doesn't implement its group controller, then the [DefaultGroupController] will
/// be used.
///
pub trait GroupController: GroupControllerActions + Send + Sync {
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str);
fn did_create_row(&mut self, row: &Row, group_id: &str);
}
/// The [GroupGenerator] trait is used to generate the groups for different [FieldType]
pub trait GroupGenerator {
type Context;
type TypeOptionType;
fn generate_groups(
field: &Field,
group_ctx: &Self::Context,
type_option: &Option<Self::TypeOptionType>,
) -> GeneratedGroupContext;
}
pub struct GeneratedGroupContext {
pub no_status_group: Option<Group>,
pub group_configs: Vec<GeneratedGroupConfig>,
}
pub struct GeneratedGroupConfig {
pub group: Group,
pub filter_content: String,
}
pub struct MoveGroupRowContext<'a> {
pub row: &'a Row,
pub row_changeset: &'a mut RowChangeset,
pub field: &'a Field,
pub to_group_id: &'a str,
pub to_row_id: Option<RowId>,
}
#[derive(Debug, Clone, Default)]
pub struct RowChangeset {
pub row_id: RowId,
pub height: Option<i32>,
pub visibility: Option<bool>,
// Contains the key/value changes represents as the update of the cells. For example,
// if there is one cell was changed, then the `cell_by_field_id` will only have one key/value.
pub cell_by_field_id: HashMap<String, Cell>,
}
impl RowChangeset {
pub fn new(row_id: RowId) -> Self {
Self {
row_id,
..Default::default()
}
}
pub fn is_empty(&self) -> bool {
self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty()
}
}
/// C: represents the group configuration that impl [GroupConfigurationSerde]
/// T: the type-option data deserializer that impl [TypeOptionDataDeserializer]
/// G: the group generator, [GroupGenerator]
/// P: the parser that impl [CellProtobufBlobParser] for the CellBytes
pub struct GenericGroupController<C, T, G, P> {
pub grouping_field_id: String,
pub type_option: Option<T>,
pub group_ctx: GroupContext<C>,
group_action_phantom: PhantomData<G>,
cell_parser_phantom: PhantomData<P>,
}
impl<C, T, G, P> GenericGroupController<C, T, G, P>
where
C: Serialize + DeserializeOwned,
T: From<TypeOptionData>,
G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
{
pub async fn new(
grouping_field: &Arc<Field>,
mut configuration: GroupContext<C>,
) -> FlowyResult<Self> {
let field_type = FieldType::from(grouping_field.field_type);
let type_option = grouping_field.get_type_option::<T>(field_type);
let generated_group_context = G::generate_groups(grouping_field, &configuration, &type_option);
let _ = configuration.init_groups(generated_group_context)?;
Ok(Self {
grouping_field_id: grouping_field.id.clone(),
type_option,
group_ctx: configuration,
group_action_phantom: PhantomData,
cell_parser_phantom: PhantomData,
})
}
// https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect
#[allow(clippy::needless_collect)]
fn update_no_status_group(
&mut self,
row: &Row,
other_group_changesets: &[GroupRowsNotificationPB],
) -> Option<GroupRowsNotificationPB> {
let no_status_group = self.group_ctx.get_mut_no_status_group()?;
// [other_group_inserted_row] contains all the inserted rows except the default group.
let other_group_inserted_row = other_group_changesets
.iter()
.flat_map(|changeset| &changeset.inserted_rows)
.collect::<Vec<&InsertedRowPB>>();
// Calculate the inserted_rows of the default_group
let no_status_group_rows = other_group_changesets
.iter()
.flat_map(|changeset| &changeset.deleted_rows)
.cloned()
.filter(|row_id| {
// if the [other_group_inserted_row] contains the row_id of the row
// which means the row should not move to the default group.
!other_group_inserted_row
.iter()
.any(|inserted_row| &inserted_row.row.id == row_id)
})
.collect::<Vec<i64>>();
let mut changeset = GroupRowsNotificationPB::new(no_status_group.id.clone());
if !no_status_group_rows.is_empty() {
changeset.inserted_rows.push(InsertedRowPB::new(row.into()));
no_status_group.add_row(row.clone());
}
// [other_group_delete_rows] contains all the deleted rows except the default group.
let other_group_delete_rows: Vec<i64> = other_group_changesets
.iter()
.flat_map(|changeset| &changeset.deleted_rows)
.cloned()
.collect();
let default_group_deleted_rows = other_group_changesets
.iter()
.flat_map(|changeset| &changeset.inserted_rows)
.filter(|inserted_row| {
// if the [other_group_delete_rows] contain the inserted_row, which means this row should move
// out from the default_group.
!other_group_delete_rows
.iter()
.any(|row_id| inserted_row.row.id == *row_id)
})
.collect::<Vec<&InsertedRowPB>>();
let mut deleted_row_ids = vec![];
for row in &no_status_group.rows {
let row_id: i64 = row.id.into();
if default_group_deleted_rows
.iter()
.any(|deleted_row| deleted_row.row.id == row_id)
{
deleted_row_ids.push(row.id);
}
}
no_status_group
.rows
.retain(|row| !deleted_row_ids.contains(&row.id));
changeset.deleted_rows.extend(
deleted_row_ids
.into_iter()
.map(|id| id.into())
.collect::<Vec<i64>>(),
);
Some(changeset)
}
}
impl<C, T, G, P> GroupControllerActions for GenericGroupController<C, T, G, P>
where
P: CellProtobufBlobParser,
C: Serialize + DeserializeOwned,
T: From<TypeOptionData>,
G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
Self: GroupCustomize<CellData = P::Object>,
{
fn field_id(&self) -> &str {
&self.grouping_field_id
}
fn groups(&self) -> Vec<&GroupData> {
self.group_ctx.groups()
}
fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)> {
let group = self.group_ctx.get_group(group_id)?;
Some((group.0, group.1.clone()))
}
#[tracing::instrument(level = "trace", skip_all, fields(row_count=%rows.len(), group_result))]
fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()> {
for row in rows {
let cell = match row.cells.get(&self.grouping_field_id) {
None => self.placeholder_cell(),
Some(cell) => Some(cell.clone()),
};
if let Some(cell) = cell {
let mut grouped_rows: Vec<GroupedRow> = vec![];
let cell_bytes = get_type_cell_protobuf(&cell, field, None);
let cell_data = cell_bytes.parser::<P>()?;
for group in self.group_ctx.groups() {
if self.can_group(&group.filter_content, &cell_data) {
grouped_rows.push(GroupedRow {
row: (*row).clone(),
group_id: group.id.clone(),
});
}
}
if !grouped_rows.is_empty() {
for group_row in grouped_rows {
if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) {
group.add_row(group_row.row);
}
}
continue;
}
}
match self.group_ctx.get_mut_no_status_group() {
None => {},
Some(no_status_group) => no_status_group.add_row((*row).clone()),
}
}
tracing::Span::current().record("group_result", format!("{},", self.group_ctx,).as_str());
Ok(())
}
fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> {
self.group_ctx.move_group(from_group_id, to_group_id)
}
fn did_update_group_row(
&mut self,
old_row: &Option<Row>,
row: &Row,
field: &Field,
) -> FlowyResult<DidUpdateGroupRowResult> {
// let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| {
// let cell_data: Option<P> = get_type_cell_data(cell_rev, field_rev, None);
// cell_data
// });
let mut result = DidUpdateGroupRowResult {
inserted_group: None,
deleted_group: None,
row_changesets: vec![],
};
if let Some(cell_data) = get_cell_data_from_row::<P>(Some(row), field) {
let old_row = old_row.as_ref();
let old_cell_data = get_cell_data_from_row::<P>(old_row, field);
if let Ok((insert, delete)) =
self.create_or_delete_group_when_cell_changed(row, old_cell_data.as_ref(), &cell_data)
{
result.inserted_group = insert;
result.deleted_group = delete;
}
let mut changesets = self.add_or_remove_row_when_cell_changed(row, &cell_data);
if let Some(changeset) = self.update_no_status_group(row, &changesets) {
if !changeset.is_empty() {
changesets.push(changeset);
}
}
result.row_changesets = changesets;
}
Ok(result)
}
fn did_delete_delete_row(
&mut self,
row: &Row,
field: &Field,
) -> FlowyResult<DidMoveGroupRowResult> {
// if the cell_rev is none, then the row must in the default group.
let mut result = DidMoveGroupRowResult {
deleted_group: None,
row_changesets: vec![],
};
if let Some(cell) = row.cells.get(&self.grouping_field_id) {
let cell_bytes = get_type_cell_protobuf(cell, field, None);
let cell_data = cell_bytes.parser::<P>()?;
if !cell_data.is_empty() {
tracing::error!("did_delete_delete_row {:?}", cell);
result.row_changesets = self.delete_row(row, &cell_data);
return Ok(result);
}
}
match self.group_ctx.get_no_status_group() {
None => {
tracing::error!("Unexpected None value. It should have the no status group");
},
Some(no_status_group) => {
if !no_status_group.contains_row(row.id) {
tracing::error!("The row: {:?} should be in the no status group", row.id);
}
result.row_changesets = vec![GroupRowsNotificationPB::delete(
no_status_group.id.clone(),
vec![row.id.into()],
)];
},
}
Ok(result)
}
#[tracing::instrument(level = "trace", skip_all, err)]
fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult> {
let mut result = DidMoveGroupRowResult {
deleted_group: None,
row_changesets: vec![],
};
let cell_rev = match context.row.cells.get(&self.grouping_field_id) {
Some(cell_rev) => Some(cell_rev.clone()),
None => self.placeholder_cell(),
};
if let Some(cell) = cell_rev {
let cell_bytes = get_type_cell_protobuf(&cell, context.field, None);
let cell_data = cell_bytes.parser::<P>()?;
result.deleted_group = self.delete_group_when_move_row(context.row, &cell_data);
result.row_changesets = self.move_row(&cell_data, context);
} else {
tracing::warn!("Unexpected moving group row, changes should not be empty");
}
Ok(result)
}
fn did_update_group_field(&mut self, _field: &Field) -> FlowyResult<Option<GroupChangesetPB>> {
Ok(None)
}
}
struct GroupedRow {
row: Row,
group_id: String,
}
fn get_cell_data_from_row<P: CellProtobufBlobParser>(
row: Option<&Row>,
field: &Field,
) -> Option<P::Object> {
let cell = row.and_then(|row| row.cells.get(&field.id))?;
let cell_bytes = get_type_cell_protobuf(cell, field, None);
cell_bytes.parser::<P>().ok()
}

Some files were not shown because too many files have changed in this diff Show More