mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #596 from AppFlowy-IO/feat/cell_filter_test
Feat/cell filter test
This commit is contained in:
commit
02cdd8ca4c
@ -9,7 +9,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
|
@ -2,8 +2,8 @@ import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
class SelectOptionService {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'dart:async';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'select_option_type_option_bloc.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
|
||||
import 'dart:async';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'select_option_type_option_bloc.dart';
|
||||
|
@ -6,7 +6,7 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
class TypeOptionService {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
|
@ -5,7 +5,7 @@ import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
|
@ -10,8 +10,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
@ -2,7 +2,7 @@ import 'dart:collection';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
|
@ -8,7 +8,7 @@ import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
@ -8,7 +8,7 @@ import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
@ -2,6 +2,7 @@
|
||||
proto_input = [
|
||||
"src/event_map.rs",
|
||||
"src/services/field/type_options",
|
||||
"src/services/field/select_option.rs",
|
||||
"src/entities",
|
||||
"src/dart_notification.rs"
|
||||
]
|
||||
|
@ -1,393 +0,0 @@
|
||||
use crate::entities::FieldType;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::parser::NotEmptyStr;
|
||||
use flowy_grid_data_model::revision::{FieldRevision, GridFilterRevision};
|
||||
use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridFilter {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct RepeatedGridFilter {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<GridFilter>,
|
||||
}
|
||||
|
||||
impl std::convert::From<&Arc<GridFilterRevision>> for GridFilter {
|
||||
fn from(rev: &Arc<GridFilterRevision>) -> Self {
|
||||
Self { id: rev.id.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&Vec<Arc<GridFilterRevision>>> for RepeatedGridFilter {
|
||||
fn from(revs: &Vec<Arc<GridFilterRevision>>) -> Self {
|
||||
RepeatedGridFilter {
|
||||
items: revs.iter().map(|rev| rev.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Vec<GridFilter>> for RepeatedGridFilter {
|
||||
fn from(items: Vec<GridFilter>) -> Self {
|
||||
Self { items }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct DeleteFilterPayload {
|
||||
#[pb(index = 1)]
|
||||
pub filter_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_type: FieldType,
|
||||
}
|
||||
|
||||
impl TryInto<DeleteFilterParams> for DeleteFilterPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<DeleteFilterParams, Self::Error> {
|
||||
let filter_id = NotEmptyStr::parse(self.filter_id)
|
||||
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
|
||||
.0;
|
||||
Ok(DeleteFilterParams {
|
||||
filter_id,
|
||||
field_type_rev: self.field_type.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct CreateGridFilterPayload {
|
||||
#[pb(index = 1)]
|
||||
pub field_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_type: FieldType,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub condition: i32,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateGridFilterPayload {
|
||||
#[allow(dead_code)]
|
||||
pub fn new<T: Into<i32>>(field_rev: &FieldRevision, condition: T, content: Option<String>) -> Self {
|
||||
Self {
|
||||
field_id: field_rev.id.clone(),
|
||||
field_type: field_rev.field_type_rev.into(),
|
||||
condition: condition.into(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<CreateGridFilterParams> for CreateGridFilterPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<CreateGridFilterParams, Self::Error> {
|
||||
let field_id = NotEmptyStr::parse(self.field_id)
|
||||
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
|
||||
.0;
|
||||
let condition = self.condition as u8;
|
||||
match self.field_type {
|
||||
FieldType::RichText | FieldType::URL => {
|
||||
let _ = TextFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::Checkbox => {
|
||||
let _ = CheckboxCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::Number => {
|
||||
let _ = NumberFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::DateTime => {
|
||||
let _ = DateFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::SingleSelect | FieldType::MultiSelect => {
|
||||
let _ = SelectOptionCondition::try_from(condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateGridFilterParams {
|
||||
field_id,
|
||||
field_type_rev: self.field_type.into(),
|
||||
condition,
|
||||
content: self.content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridTextFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: TextFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum TextFilterCondition {
|
||||
Is = 0,
|
||||
IsNot = 1,
|
||||
Contains = 2,
|
||||
DoesNotContain = 3,
|
||||
StartsWith = 4,
|
||||
EndsWith = 5,
|
||||
TextIsEmpty = 6,
|
||||
TextIsNotEmpty = 7,
|
||||
}
|
||||
impl std::convert::From<TextFilterCondition> for i32 {
|
||||
fn from(value: TextFilterCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for TextFilterCondition {
|
||||
fn default() -> Self {
|
||||
TextFilterCondition::Is
|
||||
}
|
||||
}
|
||||
impl std::convert::TryFrom<u8> for TextFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(TextFilterCondition::Is),
|
||||
1 => Ok(TextFilterCondition::IsNot),
|
||||
2 => Ok(TextFilterCondition::Contains),
|
||||
3 => Ok(TextFilterCondition::DoesNotContain),
|
||||
4 => Ok(TextFilterCondition::StartsWith),
|
||||
5 => Ok(TextFilterCondition::EndsWith),
|
||||
6 => Ok(TextFilterCondition::TextIsEmpty),
|
||||
7 => Ok(TextFilterCondition::TextIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridTextFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridTextFilter {
|
||||
condition: TextFilterCondition::try_from(rev.condition).unwrap_or(TextFilterCondition::Is),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridNumberFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: NumberFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum NumberFilterCondition {
|
||||
Equal = 0,
|
||||
NotEqual = 1,
|
||||
GreaterThan = 2,
|
||||
LessThan = 3,
|
||||
GreaterThanOrEqualTo = 4,
|
||||
LessThanOrEqualTo = 5,
|
||||
NumberIsEmpty = 6,
|
||||
NumberIsNotEmpty = 7,
|
||||
}
|
||||
impl std::default::Default for NumberFilterCondition {
|
||||
fn default() -> Self {
|
||||
NumberFilterCondition::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<NumberFilterCondition> for i32 {
|
||||
fn from(value: NumberFilterCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
impl std::convert::TryFrom<u8> for NumberFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(n: u8) -> Result<Self, Self::Error> {
|
||||
match n {
|
||||
0 => Ok(NumberFilterCondition::Equal),
|
||||
1 => Ok(NumberFilterCondition::NotEqual),
|
||||
2 => Ok(NumberFilterCondition::GreaterThan),
|
||||
3 => Ok(NumberFilterCondition::LessThan),
|
||||
4 => Ok(NumberFilterCondition::GreaterThanOrEqualTo),
|
||||
5 => Ok(NumberFilterCondition::LessThanOrEqualTo),
|
||||
6 => Ok(NumberFilterCondition::NumberIsEmpty),
|
||||
7 => Ok(NumberFilterCondition::NumberIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridNumberFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridNumberFilter {
|
||||
condition: NumberFilterCondition::try_from(rev.condition).unwrap_or(NumberFilterCondition::Equal),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridSelectOptionFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: SelectOptionCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum SelectOptionCondition {
|
||||
OptionIs = 0,
|
||||
OptionIsNot = 1,
|
||||
OptionIsEmpty = 2,
|
||||
OptionIsNotEmpty = 3,
|
||||
}
|
||||
|
||||
impl std::convert::From<SelectOptionCondition> for i32 {
|
||||
fn from(value: SelectOptionCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for SelectOptionCondition {
|
||||
fn default() -> Self {
|
||||
SelectOptionCondition::OptionIs
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for SelectOptionCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(SelectOptionCondition::OptionIs),
|
||||
1 => Ok(SelectOptionCondition::OptionIsNot),
|
||||
2 => Ok(SelectOptionCondition::OptionIsEmpty),
|
||||
3 => Ok(SelectOptionCondition::OptionIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridSelectOptionFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridSelectOptionFilter {
|
||||
condition: SelectOptionCondition::try_from(rev.condition).unwrap_or(SelectOptionCondition::OptionIs),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridDateFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: DateFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum DateFilterCondition {
|
||||
DateIs = 0,
|
||||
DateBefore = 1,
|
||||
DateAfter = 2,
|
||||
DateOnOrBefore = 3,
|
||||
DateOnOrAfter = 4,
|
||||
DateWithIn = 5,
|
||||
DateIsEmpty = 6,
|
||||
}
|
||||
|
||||
impl std::default::Default for DateFilterCondition {
|
||||
fn default() -> Self {
|
||||
DateFilterCondition::DateIs
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for DateFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(DateFilterCondition::DateIs),
|
||||
1 => Ok(DateFilterCondition::DateBefore),
|
||||
2 => Ok(DateFilterCondition::DateAfter),
|
||||
3 => Ok(DateFilterCondition::DateOnOrBefore),
|
||||
4 => Ok(DateFilterCondition::DateOnOrAfter),
|
||||
5 => Ok(DateFilterCondition::DateWithIn),
|
||||
6 => Ok(DateFilterCondition::DateIsEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridDateFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridDateFilter {
|
||||
condition: DateFilterCondition::try_from(rev.condition).unwrap_or(DateFilterCondition::DateIs),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridCheckboxFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: CheckboxCondition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum CheckboxCondition {
|
||||
IsChecked = 0,
|
||||
IsUnChecked = 1,
|
||||
}
|
||||
|
||||
impl std::convert::From<CheckboxCondition> for i32 {
|
||||
fn from(value: CheckboxCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for CheckboxCondition {
|
||||
fn default() -> Self {
|
||||
CheckboxCondition::IsChecked
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for CheckboxCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(CheckboxCondition::IsChecked),
|
||||
1 => Ok(CheckboxCondition::IsUnChecked),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridCheckboxFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridCheckboxFilter {
|
||||
condition: CheckboxCondition::try_from(rev.condition).unwrap_or(CheckboxCondition::IsChecked),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
use crate::services::field::CheckboxCellData;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridCheckboxFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: CheckboxCondition,
|
||||
}
|
||||
|
||||
impl GridCheckboxFilter {
|
||||
pub fn apply(&self, cell_data: &CheckboxCellData) -> bool {
|
||||
let is_check = cell_data.is_check();
|
||||
match self.condition {
|
||||
CheckboxCondition::IsChecked => is_check,
|
||||
CheckboxCondition::IsUnChecked => !is_check,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum CheckboxCondition {
|
||||
IsChecked = 0,
|
||||
IsUnChecked = 1,
|
||||
}
|
||||
|
||||
impl std::convert::From<CheckboxCondition> for i32 {
|
||||
fn from(value: CheckboxCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for CheckboxCondition {
|
||||
fn default() -> Self {
|
||||
CheckboxCondition::IsChecked
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for CheckboxCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(CheckboxCondition::IsChecked),
|
||||
1 => Ok(CheckboxCondition::IsUnChecked),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridCheckboxFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridCheckboxFilter {
|
||||
condition: CheckboxCondition::try_from(rev.condition).unwrap_or(CheckboxCondition::IsChecked),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::{CheckboxCondition, GridCheckboxFilter};
|
||||
use crate::services::field::CheckboxCellData;
|
||||
|
||||
#[test]
|
||||
fn checkbox_filter_is_check_test() {
|
||||
let checkbox_filter = GridCheckboxFilter {
|
||||
condition: CheckboxCondition::IsChecked,
|
||||
};
|
||||
for (value, r) in [("true", true), ("yes", true), ("false", false), ("no", false)] {
|
||||
let data = CheckboxCellData(value.to_owned());
|
||||
assert_eq!(checkbox_filter.apply(&data), r);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridDateFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: DateFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum DateFilterCondition {
|
||||
DateIs = 0,
|
||||
DateBefore = 1,
|
||||
DateAfter = 2,
|
||||
DateOnOrBefore = 3,
|
||||
DateOnOrAfter = 4,
|
||||
DateWithIn = 5,
|
||||
DateIsEmpty = 6,
|
||||
}
|
||||
|
||||
impl std::default::Default for DateFilterCondition {
|
||||
fn default() -> Self {
|
||||
DateFilterCondition::DateIs
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for DateFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(DateFilterCondition::DateIs),
|
||||
1 => Ok(DateFilterCondition::DateBefore),
|
||||
2 => Ok(DateFilterCondition::DateAfter),
|
||||
3 => Ok(DateFilterCondition::DateOnOrBefore),
|
||||
4 => Ok(DateFilterCondition::DateOnOrAfter),
|
||||
5 => Ok(DateFilterCondition::DateWithIn),
|
||||
6 => Ok(DateFilterCondition::DateIsEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridDateFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridDateFilter {
|
||||
condition: DateFilterCondition::try_from(rev.condition).unwrap_or(DateFilterCondition::DateIs),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
mod checkbox_filter;
|
||||
mod date_filter;
|
||||
mod number_filter;
|
||||
mod select_option_filter;
|
||||
mod text_filter;
|
||||
mod util;
|
||||
|
||||
pub use checkbox_filter::*;
|
||||
pub use date_filter::*;
|
||||
pub use number_filter::*;
|
||||
pub use select_option_filter::*;
|
||||
pub use text_filter::*;
|
||||
pub use util::*;
|
@ -0,0 +1,142 @@
|
||||
use crate::services::field::NumberCellData;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
use rust_decimal::prelude::Zero;
|
||||
use rust_decimal::Decimal;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridNumberFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: NumberFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
impl GridNumberFilter {
|
||||
pub fn apply(&self, num_cell_data: &NumberCellData) -> bool {
|
||||
if self.content.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let content = self.content.as_ref().unwrap();
|
||||
let zero_decimal = Decimal::zero();
|
||||
let cell_decimal = num_cell_data.decimal().as_ref().unwrap_or(&zero_decimal);
|
||||
match Decimal::from_str(content) {
|
||||
Ok(decimal) => match self.condition {
|
||||
NumberFilterCondition::Equal => cell_decimal == &decimal,
|
||||
NumberFilterCondition::NotEqual => cell_decimal != &decimal,
|
||||
NumberFilterCondition::GreaterThan => cell_decimal > &decimal,
|
||||
NumberFilterCondition::LessThan => cell_decimal < &decimal,
|
||||
NumberFilterCondition::GreaterThanOrEqualTo => cell_decimal >= &decimal,
|
||||
NumberFilterCondition::LessThanOrEqualTo => cell_decimal <= &decimal,
|
||||
NumberFilterCondition::NumberIsEmpty => num_cell_data.is_empty(),
|
||||
NumberFilterCondition::NumberIsNotEmpty => !num_cell_data.is_empty(),
|
||||
},
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum NumberFilterCondition {
|
||||
Equal = 0,
|
||||
NotEqual = 1,
|
||||
GreaterThan = 2,
|
||||
LessThan = 3,
|
||||
GreaterThanOrEqualTo = 4,
|
||||
LessThanOrEqualTo = 5,
|
||||
NumberIsEmpty = 6,
|
||||
NumberIsNotEmpty = 7,
|
||||
}
|
||||
|
||||
impl std::default::Default for NumberFilterCondition {
|
||||
fn default() -> Self {
|
||||
NumberFilterCondition::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<NumberFilterCondition> for i32 {
|
||||
fn from(value: NumberFilterCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
impl std::convert::TryFrom<u8> for NumberFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(n: u8) -> Result<Self, Self::Error> {
|
||||
match n {
|
||||
0 => Ok(NumberFilterCondition::Equal),
|
||||
1 => Ok(NumberFilterCondition::NotEqual),
|
||||
2 => Ok(NumberFilterCondition::GreaterThan),
|
||||
3 => Ok(NumberFilterCondition::LessThan),
|
||||
4 => Ok(NumberFilterCondition::GreaterThanOrEqualTo),
|
||||
5 => Ok(NumberFilterCondition::LessThanOrEqualTo),
|
||||
6 => Ok(NumberFilterCondition::NumberIsEmpty),
|
||||
7 => Ok(NumberFilterCondition::NumberIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridNumberFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridNumberFilter {
|
||||
condition: NumberFilterCondition::try_from(rev.condition).unwrap_or(NumberFilterCondition::Equal),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::{GridNumberFilter, NumberFilterCondition};
|
||||
|
||||
use crate::services::field::{NumberCellData, NumberFormat};
|
||||
use std::str::FromStr;
|
||||
#[test]
|
||||
fn number_filter_equal_test() {
|
||||
let number_filter = GridNumberFilter {
|
||||
condition: NumberFilterCondition::Equal,
|
||||
content: Some("123".to_owned()),
|
||||
};
|
||||
|
||||
for (num_str, r) in [("123", true), ("1234", false), ("", false)] {
|
||||
let data = NumberCellData::from_str(num_str).unwrap();
|
||||
assert_eq!(number_filter.apply(&data), r);
|
||||
}
|
||||
|
||||
let format = NumberFormat::USD;
|
||||
for (num_str, r) in [("$123", true), ("1234", false), ("", false)] {
|
||||
let data = NumberCellData::from_format_str(num_str, true, &format).unwrap();
|
||||
assert_eq!(number_filter.apply(&data), r);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn number_filter_greater_than_test() {
|
||||
let number_filter = GridNumberFilter {
|
||||
condition: NumberFilterCondition::GreaterThan,
|
||||
content: Some("12".to_owned()),
|
||||
};
|
||||
for (num_str, r) in [("123", true), ("10", false), ("30", true), ("", false)] {
|
||||
let data = NumberCellData::from_str(num_str).unwrap();
|
||||
assert_eq!(number_filter.apply(&data), r);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_filter_less_than_test() {
|
||||
let number_filter = GridNumberFilter {
|
||||
condition: NumberFilterCondition::LessThan,
|
||||
content: Some("100".to_owned()),
|
||||
};
|
||||
for (num_str, r) in [("12", true), ("1234", false), ("30", true), ("", true)] {
|
||||
let data = NumberCellData::from_str(num_str).unwrap();
|
||||
assert_eq!(number_filter.apply(&data), r);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
#![allow(clippy::needless_collect)]
|
||||
use crate::services::field::select_option::{SelectOptionIds, SelectedSelectOptions};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridSelectOptionFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: SelectOptionCondition,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub option_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl GridSelectOptionFilter {
|
||||
pub fn apply(&self, selected_options: &SelectedSelectOptions) -> bool {
|
||||
let selected_option_ids: Vec<&String> = selected_options.options.iter().map(|option| &option.id).collect();
|
||||
match self.condition {
|
||||
SelectOptionCondition::OptionIs => {
|
||||
// if selected options equal to filter's options, then the required_options will be empty.
|
||||
let required_options = self
|
||||
.option_ids
|
||||
.iter()
|
||||
.filter(|id| !selected_option_ids.contains(id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect
|
||||
!required_options.is_empty()
|
||||
}
|
||||
SelectOptionCondition::OptionIsNot => {
|
||||
for option_id in selected_option_ids {
|
||||
if self.option_ids.contains(option_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
SelectOptionCondition::OptionIsEmpty => selected_option_ids.is_empty(),
|
||||
SelectOptionCondition::OptionIsNotEmpty => !selected_option_ids.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum SelectOptionCondition {
|
||||
OptionIs = 0,
|
||||
OptionIsNot = 1,
|
||||
OptionIsEmpty = 2,
|
||||
OptionIsNotEmpty = 3,
|
||||
}
|
||||
|
||||
impl std::convert::From<SelectOptionCondition> for i32 {
|
||||
fn from(value: SelectOptionCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for SelectOptionCondition {
|
||||
fn default() -> Self {
|
||||
SelectOptionCondition::OptionIs
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u8> for SelectOptionCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(SelectOptionCondition::OptionIs),
|
||||
1 => Ok(SelectOptionCondition::OptionIsNot),
|
||||
2 => Ok(SelectOptionCondition::OptionIsEmpty),
|
||||
3 => Ok(SelectOptionCondition::OptionIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridSelectOptionFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
let ids = SelectOptionIds::from(rev.content.clone());
|
||||
GridSelectOptionFilter {
|
||||
condition: SelectOptionCondition::try_from(rev.condition).unwrap_or(SelectOptionCondition::OptionIs),
|
||||
option_ids: ids.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::all)]
|
||||
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
||||
use crate::services::field::select_option::{SelectOption, SelectedSelectOptions};
|
||||
|
||||
#[test]
|
||||
fn select_option_filter_is_test() {
|
||||
let option_1 = SelectOption::new("A");
|
||||
let option_2 = SelectOption::new("B");
|
||||
|
||||
let filter_1 = GridSelectOptionFilter {
|
||||
condition: SelectOptionCondition::OptionIs,
|
||||
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
filter_1.apply(&SelectedSelectOptions {
|
||||
options: vec![option_1.clone(), option_2.clone()],
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
filter_1.apply(&SelectedSelectOptions {
|
||||
options: vec![option_1.clone()],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridTextFilter {
|
||||
#[pb(index = 1)]
|
||||
pub condition: TextFilterCondition,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
impl GridTextFilter {
|
||||
pub fn apply<T: AsRef<str>>(&self, cell_data: T) -> bool {
|
||||
let cell_data = cell_data.as_ref();
|
||||
let s = cell_data.to_lowercase();
|
||||
if let Some(content) = self.content.as_ref() {
|
||||
match self.condition {
|
||||
TextFilterCondition::Is => &s == content,
|
||||
TextFilterCondition::IsNot => &s != content,
|
||||
TextFilterCondition::Contains => s.contains(content),
|
||||
TextFilterCondition::DoesNotContain => !s.contains(content),
|
||||
TextFilterCondition::StartsWith => s.starts_with(content),
|
||||
TextFilterCondition::EndsWith => s.ends_with(content),
|
||||
TextFilterCondition::TextIsEmpty => s.is_empty(),
|
||||
TextFilterCondition::TextIsNotEmpty => !s.is_empty(),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum TextFilterCondition {
|
||||
Is = 0,
|
||||
IsNot = 1,
|
||||
Contains = 2,
|
||||
DoesNotContain = 3,
|
||||
StartsWith = 4,
|
||||
EndsWith = 5,
|
||||
TextIsEmpty = 6,
|
||||
TextIsNotEmpty = 7,
|
||||
}
|
||||
|
||||
impl std::convert::From<TextFilterCondition> for i32 {
|
||||
fn from(value: TextFilterCondition) -> Self {
|
||||
value as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for TextFilterCondition {
|
||||
fn default() -> Self {
|
||||
TextFilterCondition::Is
|
||||
}
|
||||
}
|
||||
impl std::convert::TryFrom<u8> for TextFilterCondition {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(TextFilterCondition::Is),
|
||||
1 => Ok(TextFilterCondition::IsNot),
|
||||
2 => Ok(TextFilterCondition::Contains),
|
||||
3 => Ok(TextFilterCondition::DoesNotContain),
|
||||
4 => Ok(TextFilterCondition::StartsWith),
|
||||
5 => Ok(TextFilterCondition::EndsWith),
|
||||
6 => Ok(TextFilterCondition::TextIsEmpty),
|
||||
7 => Ok(TextFilterCondition::TextIsNotEmpty),
|
||||
_ => Err(ErrorCode::InvalidData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Arc<GridFilterRevision>> for GridTextFilter {
|
||||
fn from(rev: Arc<GridFilterRevision>) -> Self {
|
||||
GridTextFilter {
|
||||
condition: TextFilterCondition::try_from(rev.condition).unwrap_or(TextFilterCondition::Is),
|
||||
content: rev.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::all)]
|
||||
use crate::entities::{GridTextFilter, TextFilterCondition};
|
||||
|
||||
#[test]
|
||||
fn text_filter_equal_test() {
|
||||
let text_filter = GridTextFilter {
|
||||
condition: TextFilterCondition::Is,
|
||||
content: Some("appflowy".to_owned()),
|
||||
};
|
||||
|
||||
assert!(text_filter.apply("AppFlowy"));
|
||||
assert_eq!(text_filter.apply("appflowy"), true);
|
||||
assert_eq!(text_filter.apply("Appflowy"), true);
|
||||
assert_eq!(text_filter.apply("AppFlowy.io"), false);
|
||||
}
|
||||
#[test]
|
||||
fn text_filter_start_with_test() {
|
||||
let text_filter = GridTextFilter {
|
||||
condition: TextFilterCondition::StartsWith,
|
||||
content: Some("appflowy".to_owned()),
|
||||
};
|
||||
|
||||
assert_eq!(text_filter.apply("AppFlowy.io"), true);
|
||||
assert_eq!(text_filter.apply(""), false);
|
||||
assert_eq!(text_filter.apply("https"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_filter_end_with_test() {
|
||||
let text_filter = GridTextFilter {
|
||||
condition: TextFilterCondition::EndsWith,
|
||||
content: Some("appflowy".to_owned()),
|
||||
};
|
||||
|
||||
assert_eq!(text_filter.apply("https://github.com/appflowy"), true);
|
||||
assert_eq!(text_filter.apply("App"), false);
|
||||
assert_eq!(text_filter.apply("appflowy.io"), false);
|
||||
}
|
||||
#[test]
|
||||
fn text_filter_empty_test() {
|
||||
let text_filter = GridTextFilter {
|
||||
condition: TextFilterCondition::TextIsEmpty,
|
||||
content: Some("appflowy".to_owned()),
|
||||
};
|
||||
|
||||
assert_eq!(text_filter.apply(""), true);
|
||||
assert_eq!(text_filter.apply("App"), false);
|
||||
}
|
||||
#[test]
|
||||
fn text_filter_contain_test() {
|
||||
let text_filter = GridTextFilter {
|
||||
condition: TextFilterCondition::Contains,
|
||||
content: Some("appflowy".to_owned()),
|
||||
};
|
||||
|
||||
assert_eq!(text_filter.apply("https://github.com/appflowy"), true);
|
||||
assert_eq!(text_filter.apply("AppFlowy"), true);
|
||||
assert_eq!(text_filter.apply("App"), false);
|
||||
assert_eq!(text_filter.apply(""), false);
|
||||
assert_eq!(text_filter.apply("github"), false);
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
use crate::entities::{
|
||||
CheckboxCondition, DateFilterCondition, FieldType, NumberFilterCondition, SelectOptionCondition,
|
||||
TextFilterCondition,
|
||||
};
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::parser::NotEmptyStr;
|
||||
use flowy_grid_data_model::revision::{FieldRevision, GridFilterRevision};
|
||||
use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct GridFilter {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct RepeatedGridFilter {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<GridFilter>,
|
||||
}
|
||||
|
||||
impl std::convert::From<&Arc<GridFilterRevision>> for GridFilter {
|
||||
fn from(rev: &Arc<GridFilterRevision>) -> Self {
|
||||
Self { id: rev.id.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&Vec<Arc<GridFilterRevision>>> for RepeatedGridFilter {
|
||||
fn from(revs: &Vec<Arc<GridFilterRevision>>) -> Self {
|
||||
RepeatedGridFilter {
|
||||
items: revs.iter().map(|rev| rev.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Vec<GridFilter>> for RepeatedGridFilter {
|
||||
fn from(items: Vec<GridFilter>) -> Self {
|
||||
Self { items }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct DeleteFilterPayload {
|
||||
#[pb(index = 1)]
|
||||
pub filter_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_type: FieldType,
|
||||
}
|
||||
|
||||
impl TryInto<DeleteFilterParams> for DeleteFilterPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<DeleteFilterParams, Self::Error> {
|
||||
let filter_id = NotEmptyStr::parse(self.filter_id)
|
||||
.map_err(|_| ErrorCode::UnexpectedEmptyString)?
|
||||
.0;
|
||||
Ok(DeleteFilterParams {
|
||||
filter_id,
|
||||
field_type_rev: self.field_type.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct CreateGridFilterPayload {
|
||||
#[pb(index = 1)]
|
||||
pub field_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_type: FieldType,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub condition: i32,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateGridFilterPayload {
|
||||
#[allow(dead_code)]
|
||||
pub fn new<T: Into<i32>>(field_rev: &FieldRevision, condition: T, content: Option<String>) -> Self {
|
||||
Self {
|
||||
field_id: field_rev.id.clone(),
|
||||
field_type: field_rev.field_type_rev.into(),
|
||||
condition: condition.into(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<CreateGridFilterParams> for CreateGridFilterPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<CreateGridFilterParams, Self::Error> {
|
||||
let field_id = NotEmptyStr::parse(self.field_id)
|
||||
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
|
||||
.0;
|
||||
let condition = self.condition as u8;
|
||||
match self.field_type {
|
||||
FieldType::RichText | FieldType::URL => {
|
||||
let _ = TextFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::Checkbox => {
|
||||
let _ = CheckboxCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::Number => {
|
||||
let _ = NumberFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::DateTime => {
|
||||
let _ = DateFilterCondition::try_from(condition)?;
|
||||
}
|
||||
FieldType::SingleSelect | FieldType::MultiSelect => {
|
||||
let _ = SelectOptionCondition::try_from(condition)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateGridFilterParams {
|
||||
field_id,
|
||||
field_type_rev: self.field_type.into(),
|
||||
condition,
|
||||
content: self.content,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
use crate::entities::*;
|
||||
use crate::manager::GridManager;
|
||||
use crate::services::field::type_options::*;
|
||||
use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_json_str};
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::{
|
||||
default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload,
|
||||
};
|
||||
use crate::services::row::AnyCellData;
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
||||
use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams};
|
||||
@ -362,7 +365,8 @@ pub(crate) async fn get_select_option_handler(
|
||||
Some(field_rev) => {
|
||||
let cell_rev = editor.get_cell_rev(¶ms.row_id, ¶ms.field_id).await?;
|
||||
let type_option = select_option_operation(&field_rev)?;
|
||||
let option_context = type_option.select_option_cell_data(&cell_rev);
|
||||
let any_cell_data: AnyCellData = cell_rev.try_into()?;
|
||||
let option_context = type_option.selected_select_option(any_cell_data);
|
||||
data_result(option_context)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod field_builder;
|
||||
pub mod select_option;
|
||||
pub(crate) mod type_options;
|
||||
|
||||
pub use field_builder::*;
|
||||
|
337
frontend/rust-lib/flowy-grid/src/services/field/select_option.rs
Normal file
337
frontend/rust-lib/flowy-grid/src/services/field/select_option.rs
Normal file
@ -0,0 +1,337 @@
|
||||
use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType};
|
||||
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
use crate::services::row::AnyCellData;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::parser::NotEmptyStr;
|
||||
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry};
|
||||
use nanoid::nanoid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const SELECTION_IDS_SEPARATOR: &str = ",";
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SelectOption {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub color: SelectOptionColor,
|
||||
}
|
||||
|
||||
impl SelectOption {
|
||||
pub fn new(name: &str) -> Self {
|
||||
SelectOption {
|
||||
id: nanoid!(4),
|
||||
name: name.to_owned(),
|
||||
color: SelectOptionColor::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_color(name: &str, color: SelectOptionColor) -> Self {
|
||||
SelectOption {
|
||||
id: nanoid!(4),
|
||||
name: name.to_owned(),
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, 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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_selected_select_options<T: TryInto<AnyCellData>>(
|
||||
any_cell_data: T,
|
||||
options: &[SelectOption],
|
||||
) -> Vec<SelectOption> {
|
||||
if let Ok(type_option_cell_data) = any_cell_data.try_into() {
|
||||
let ids = SelectOptionIds::from(type_option_cell_data.cell_data);
|
||||
ids.iter()
|
||||
.flat_map(|option_id| options.iter().find(|option| &option.id == option_id).cloned())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
|
||||
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 = select_option_color_from_index(self.options().len());
|
||||
SelectOption::with_color(name, color)
|
||||
}
|
||||
|
||||
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData;
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption>;
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
|
||||
}
|
||||
|
||||
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
match &field_type {
|
||||
FieldType::SingleSelect => {
|
||||
let type_option = SingleSelectTypeOption::from(field_rev);
|
||||
Ok(Box::new(type_option))
|
||||
}
|
||||
FieldType::MultiSelect => {
|
||||
let type_option = MultiSelectTypeOption::from(field_rev);
|
||||
Ok(Box::new(type_option))
|
||||
}
|
||||
ty => {
|
||||
tracing::error!("Unsupported field type: {:?} for this handler", ty);
|
||||
Err(ErrorCode::FieldInvalidOperation.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_option_color_from_index(index: usize) -> SelectOptionColor {
|
||||
match index % 8 {
|
||||
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 SelectOptionIds(Vec<String>);
|
||||
|
||||
impl SelectOptionIds {
|
||||
pub fn into_inner(self) -> Vec<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<AnyCellData> for SelectOptionIds {
|
||||
type Error = FlowyError;
|
||||
|
||||
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
|
||||
Ok(Self::from(value.cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<String> for SelectOptionIds {
|
||||
fn from(s: String) -> Self {
|
||||
let ids = s
|
||||
.split(SELECTION_IDS_SEPARATOR)
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
Self(ids)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Option<String>> for SelectOptionIds {
|
||||
fn from(s: Option<String>) -> Self {
|
||||
match s {
|
||||
None => Self { 0: 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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct SelectOptionCellChangesetPayload {
|
||||
#[pb(index = 1)]
|
||||
pub cell_identifier: CellIdentifierPayload,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub insert_option_id: Option<String>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SelectOptionCellChangesetParams {
|
||||
pub cell_identifier: CellIdentifier,
|
||||
pub insert_option_id: Option<String>,
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
impl std::convert::From<SelectOptionCellChangesetParams> for CellChangeset {
|
||||
fn from(params: SelectOptionCellChangesetParams) -> Self {
|
||||
let changeset = SelectOptionCellContentChangeset {
|
||||
insert_option_id: params.insert_option_id,
|
||||
delete_option_id: params.delete_option_id,
|
||||
};
|
||||
let s = serde_json::to_string(&changeset).unwrap();
|
||||
CellChangeset {
|
||||
grid_id: params.cell_identifier.grid_id,
|
||||
row_id: params.cell_identifier.row_id,
|
||||
field_id: params.cell_identifier.field_id,
|
||||
cell_content_changeset: Some(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
|
||||
let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?;
|
||||
let insert_option_id = match self.insert_option_id {
|
||||
None => None,
|
||||
Some(insert_option_id) => Some(
|
||||
NotEmptyStr::parse(insert_option_id)
|
||||
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
let delete_option_id = match self.delete_option_id {
|
||||
None => None,
|
||||
Some(delete_option_id) => Some(
|
||||
NotEmptyStr::parse(delete_option_id)
|
||||
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(SelectOptionCellChangesetParams {
|
||||
cell_identifier,
|
||||
insert_option_id,
|
||||
delete_option_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SelectOptionCellContentChangeset {
|
||||
pub insert_option_id: Option<String>,
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
impl SelectOptionCellContentChangeset {
|
||||
pub fn from_insert(option_id: &str) -> Self {
|
||||
SelectOptionCellContentChangeset {
|
||||
insert_option_id: Some(option_id.to_string()),
|
||||
delete_option_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_delete(option_id: &str) -> Self {
|
||||
SelectOptionCellContentChangeset {
|
||||
insert_option_id: None,
|
||||
delete_option_id: Some(option_id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SelectOptionCellData {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub select_options: Vec<SelectOption>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct SelectOptionChangesetPayload {
|
||||
#[pb(index = 1)]
|
||||
pub cell_identifier: CellIdentifierPayload,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub insert_option: Option<SelectOption>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub update_option: Option<SelectOption>,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub delete_option: Option<SelectOption>,
|
||||
}
|
||||
|
||||
pub struct SelectOptionChangeset {
|
||||
pub cell_identifier: CellIdentifier,
|
||||
pub insert_option: Option<SelectOption>,
|
||||
pub update_option: Option<SelectOption>,
|
||||
pub delete_option: Option<SelectOption>,
|
||||
}
|
||||
|
||||
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<SelectOptionChangeset, Self::Error> {
|
||||
let cell_identifier = self.cell_identifier.try_into()?;
|
||||
Ok(SelectOptionChangeset {
|
||||
cell_identifier,
|
||||
insert_option: self.insert_option,
|
||||
update_option: self.update_option,
|
||||
delete_option: self.delete_option,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -42,9 +42,13 @@ impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
|
||||
const YES: &str = "Yes";
|
||||
const NO: &str = "No";
|
||||
|
||||
impl CellFilterOperation<GridCheckboxFilter, CheckboxCellData> for CheckboxTypeOption {
|
||||
fn apply_filter(&self, _cell_data: CheckboxCellData, _filter: &GridCheckboxFilter) -> bool {
|
||||
false
|
||||
impl CellFilterOperation<GridCheckboxFilter> for CheckboxTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridCheckboxFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_checkbox() {
|
||||
return Ok(true);
|
||||
}
|
||||
let checkbox_cell_data: CheckboxCellData = any_cell_data.try_into()?;
|
||||
Ok(filter.apply(&checkbox_cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,10 +100,18 @@ fn string_to_bool(bool_str: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckboxCellData(String);
|
||||
impl std::convert::From<AnyCellData> for CheckboxCellData {
|
||||
fn from(any_cell_data: AnyCellData) -> Self {
|
||||
CheckboxCellData(any_cell_data.cell_data)
|
||||
pub struct CheckboxCellData(pub String);
|
||||
|
||||
impl CheckboxCellData {
|
||||
pub fn is_check(&self) -> bool {
|
||||
string_to_bool(&self.0)
|
||||
}
|
||||
}
|
||||
impl std::convert::TryFrom<AnyCellData> for CheckboxCellData {
|
||||
type Error = FlowyError;
|
||||
|
||||
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,9 +117,12 @@ impl DateTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
impl CellFilterOperation<GridDateFilter, AnyCellData> for DateTypeOption {
|
||||
fn apply_filter(&self, _cell_data: AnyCellData, _filter: &GridDateFilter) -> bool {
|
||||
false
|
||||
impl CellFilterOperation<GridDateFilter> for DateTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridDateFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_date() {
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
mod checkbox_type_option;
|
||||
mod date_type_option;
|
||||
mod multi_select_type_option;
|
||||
mod number_type_option;
|
||||
mod selection_type_option;
|
||||
mod single_select_type_option;
|
||||
mod text_type_option;
|
||||
mod url_type_option;
|
||||
mod util;
|
||||
|
||||
pub use checkbox_type_option::*;
|
||||
pub use date_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use number_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use single_select_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use url_type_option::*;
|
||||
|
@ -0,0 +1,217 @@
|
||||
use crate::entities::{FieldType, GridSelectOptionFilter};
|
||||
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::select_option::{
|
||||
make_selected_select_options, SelectOption, SelectOptionCellContentChangeset, SelectOptionCellData,
|
||||
SelectOptionIds, SelectOptionOperation, SelectedSelectOptions, SELECTION_IDS_SEPARATOR,
|
||||
};
|
||||
use crate::services::field::type_options::util::get_cell_data;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{
|
||||
AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Multiple select
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct MultiSelectTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub disable_color: bool,
|
||||
}
|
||||
impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect);
|
||||
|
||||
impl SelectOptionOperation for MultiSelectTypeOption {
|
||||
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData {
|
||||
let select_options = make_selected_select_options(any_cell_data, &self.options);
|
||||
SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
impl CellFilterOperation<GridSelectOptionFilter> for MultiSelectTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_multi_select() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data));
|
||||
Ok(filter.apply(&selected_options))
|
||||
}
|
||||
}
|
||||
impl CellDataOperation<String> for MultiSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
cell_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if !decoded_field_type.is_select_option() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let encoded_data = cell_data.into();
|
||||
let ids: SelectOptionIds = encoded_data.into();
|
||||
let select_options = ids
|
||||
.iter()
|
||||
.flat_map(|option_id| self.options.iter().find(|option| &option.id == option_id).cloned())
|
||||
.collect::<Vec<SelectOption>>();
|
||||
|
||||
let cell_data = SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
};
|
||||
|
||||
DecodedCellData::try_from_bytes(cell_data)
|
||||
}
|
||||
|
||||
fn apply_changeset<T>(&self, changeset: T, cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
|
||||
where
|
||||
T: Into<CellContentChangeset>,
|
||||
{
|
||||
let content_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset.into())?;
|
||||
let new_cell_data: String;
|
||||
match cell_rev {
|
||||
None => {
|
||||
new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned());
|
||||
}
|
||||
Some(cell_rev) => {
|
||||
let cell_data = get_cell_data(&cell_rev);
|
||||
let mut select_ids: SelectOptionIds = cell_data.into();
|
||||
if let Some(insert_option_id) = content_changeset.insert_option_id {
|
||||
tracing::trace!("Insert multi select option: {}", &insert_option_id);
|
||||
if select_ids.contains(&insert_option_id) {
|
||||
select_ids.retain(|id| id != &insert_option_id);
|
||||
} else {
|
||||
select_ids.push(insert_option_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delete_option_id) = content_changeset.delete_option_id {
|
||||
tracing::trace!("Delete multi select option: {}", &delete_option_id);
|
||||
select_ids.retain(|id| id != &delete_option_id);
|
||||
}
|
||||
|
||||
new_cell_data = select_ids.join(SELECTION_IDS_SEPARATOR);
|
||||
tracing::trace!("Multi select cell data: {}", &new_cell_data);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption);
|
||||
impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption);
|
||||
impl MultiSelectTypeOptionBuilder {
|
||||
pub fn option(mut self, opt: SelectOption) -> Self {
|
||||
self.0.options.push(opt);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
FieldType::MultiSelect
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
||||
|
||||
#[test]
|
||||
fn multi_select_test() {
|
||||
let google_option = SelectOption::new("Google");
|
||||
let facebook_option = SelectOption::new("Facebook");
|
||||
let twitter_option = SelectOption::new("Twitter");
|
||||
let multi_select = MultiSelectTypeOptionBuilder::default()
|
||||
.option(google_option.clone())
|
||||
.option(facebook_option.clone())
|
||||
.option(twitter_option);
|
||||
|
||||
let field_rev = FieldBuilder::new(multi_select)
|
||||
.name("Platform")
|
||||
.visibility(true)
|
||||
.build();
|
||||
|
||||
let type_option = MultiSelectTypeOption::from(&field_rev);
|
||||
|
||||
let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR);
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_multi_select_options(
|
||||
cell_data,
|
||||
&type_option,
|
||||
&field_rev,
|
||||
vec![google_option.clone(), facebook_option],
|
||||
);
|
||||
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None)
|
||||
.unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("123,456").to_str(), None)
|
||||
.unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid changeset
|
||||
assert!(type_option.apply_changeset("123", None).is_err());
|
||||
}
|
||||
|
||||
fn assert_multi_select_options(
|
||||
cell_data: String,
|
||||
type_option: &MultiSelectTypeOption,
|
||||
field_rev: &FieldRevision,
|
||||
expected: Vec<SelectOption>,
|
||||
) {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
assert_eq!(
|
||||
expected,
|
||||
type_option
|
||||
.decode_cell_data(cell_data, &field_type, field_rev)
|
||||
.unwrap()
|
||||
.parse::<SelectOptionCellData>()
|
||||
.unwrap()
|
||||
.select_options,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use crate::impl_type_option;
|
||||
|
||||
use crate::entities::{FieldType, GridNumberFilter};
|
||||
use crate::services::field::number_currency::Currency;
|
||||
use crate::services::field::type_options::number_type_option::format::*;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{
|
||||
@ -10,7 +11,9 @@ use bytes::Bytes;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use rusty_money::Money;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
@ -76,24 +79,13 @@ impl NumberTypeOption {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn cell_content_from_number_str(&self, s: &str) -> FlowyResult<String> {
|
||||
fn format_cell_data(&self, s: &str) -> FlowyResult<NumberCellData> {
|
||||
match self.format {
|
||||
NumberFormat::Num => {
|
||||
if let Ok(v) = s.parse::<f64>() {
|
||||
return Ok(v.to_string());
|
||||
}
|
||||
|
||||
if let Ok(v) = s.parse::<i64>() {
|
||||
return Ok(v.to_string());
|
||||
}
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = s.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Ok(content)
|
||||
}
|
||||
_ => self.money_from_number_str(s),
|
||||
NumberFormat::Num | NumberFormat::Percent => match Decimal::from_str(s) {
|
||||
Ok(value, ..) => Ok(NumberCellData::from_decimal(value)),
|
||||
Err(_) => Ok(NumberCellData::new()),
|
||||
},
|
||||
_ => NumberCellData::from_format_str(s, self.sign_positive, &self.format),
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,49 +93,28 @@ impl NumberTypeOption {
|
||||
self.format = format;
|
||||
self.symbol = format.symbol();
|
||||
}
|
||||
|
||||
fn money_from_number_str(&self, s: &str) -> FlowyResult<String> {
|
||||
let mut number = self.strip_currency_symbol(s);
|
||||
|
||||
if s.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
match Decimal::from_str(&number) {
|
||||
Ok(mut decimal) => {
|
||||
decimal.set_sign_positive(self.sign_positive);
|
||||
let money = rusty_money::Money::from_decimal(decimal, self.format.currency()).to_string();
|
||||
Ok(money)
|
||||
}
|
||||
Err(_) => match rusty_money::Money::from_str(&number, self.format.currency()) {
|
||||
Ok(money) => Ok(money.to_string()),
|
||||
Err(_) => {
|
||||
number.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
|
||||
if number.chars().all(char::is_numeric) {
|
||||
self.money_from_number_str(&number)
|
||||
} else {
|
||||
Err(FlowyError::invalid_data().context("Should only contain numbers"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_currency_symbol<T: ToString>(&self, 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 CellFilterOperation<GridNumberFilter, AnyCellData> for NumberTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridNumberFilter) -> bool {
|
||||
let _number_cell_data = NumberCellData::from_number_type_option(self, any_cell_data);
|
||||
false
|
||||
|
||||
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 CellFilterOperation<GridNumberFilter> for NumberTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridNumberFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_number() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let cell_data = any_cell_data.cell_data;
|
||||
let num_cell_data = self.format_cell_data(&cell_data)?;
|
||||
|
||||
Ok(filter.apply(&num_cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,28 +133,9 @@ impl CellDataOperation<String> for NumberTypeOption {
|
||||
}
|
||||
|
||||
let cell_data = cell_data.into();
|
||||
match self.format {
|
||||
NumberFormat::Num => {
|
||||
if let Ok(v) = cell_data.parse::<f64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
if let Ok(v) = cell_data.parse::<i64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
Ok(DecodedCellData::default())
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
_ => {
|
||||
let content = self
|
||||
.money_from_number_str(&cell_data)
|
||||
.unwrap_or_else(|_| "".to_string());
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
match self.format_cell_data(&cell_data) {
|
||||
Ok(num) => Ok(DecodedCellData::new(num.to_string())),
|
||||
Err(_) => Ok(DecodedCellData::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +145,7 @@ impl CellDataOperation<String> for NumberTypeOption {
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let data = changeset.trim().to_string();
|
||||
let _ = self.cell_content_from_number_str(&data)?;
|
||||
let _ = self.format_cell_data(&data)?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@ -213,33 +165,88 @@ impl std::default::Default for NumberTypeOption {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NumberCellData(String);
|
||||
pub struct NumberCellData {
|
||||
decimal: Option<Decimal>,
|
||||
money: Option<String>,
|
||||
}
|
||||
|
||||
impl NumberCellData {
|
||||
fn from_number_type_option(type_option: &NumberTypeOption, any_cell_data: AnyCellData) -> Self {
|
||||
let cell_data = any_cell_data.cell_data;
|
||||
match type_option.format {
|
||||
NumberFormat::Num => {
|
||||
if let Ok(v) = cell_data.parse::<f64>() {
|
||||
return Self(v.to_string());
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
decimal: Default::default(),
|
||||
money: None,
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(v) = cell_data.parse::<i64>() {
|
||||
return Self(v.to_string());
|
||||
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(NumberCellData::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 {
|
||||
Err(FlowyError::invalid_data().context("Should only contain numbers"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Self::default()
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Self(content)
|
||||
}
|
||||
_ => {
|
||||
let content = type_option
|
||||
.money_from_number_str(&cell_data)
|
||||
.unwrap_or_else(|_| "".to_string());
|
||||
Self(content)
|
||||
}
|
||||
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 = rust_decimal::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.is_empty() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
let decimal = Decimal::from_str(s)?;
|
||||
Ok(Self::from_decimal(decimal))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for NumberCellData {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,7 +255,7 @@ impl NumberCellData {
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{NumberFormat, NumberTypeOption};
|
||||
use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
||||
use strum::IntoEnumIterator;
|
||||
@ -266,10 +273,10 @@ mod tests {
|
||||
fn number_type_option_strip_symbol_test() {
|
||||
let mut type_option = NumberTypeOption::new();
|
||||
type_option.format = NumberFormat::USD;
|
||||
assert_eq!(type_option.strip_currency_symbol("$18,443"), "18,443".to_owned());
|
||||
assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned());
|
||||
|
||||
type_option.format = NumberFormat::Yuan;
|
||||
assert_eq!(type_option.strip_currency_symbol("$0.2"), "0.2".to_owned());
|
||||
assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,660 +0,0 @@
|
||||
use crate::entities::{CellChangeset, FieldType, GridSelectOptionFilter};
|
||||
use crate::entities::{CellIdentifier, CellIdentifierPayload};
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::type_options::util::get_cell_data;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{
|
||||
AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::parser::NotEmptyStr;
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||
use nanoid::nanoid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub const SELECTION_IDS_SEPARATOR: &str = ",";
|
||||
|
||||
pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
|
||||
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 = select_option_color_from_index(self.options().len());
|
||||
SelectOption::with_color(name, color)
|
||||
}
|
||||
|
||||
fn select_option_cell_data(&self, cell_rev: &Option<CellRevision>) -> SelectOptionCellData;
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption>;
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
|
||||
}
|
||||
|
||||
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
match &field_type {
|
||||
FieldType::SingleSelect => {
|
||||
let type_option = SingleSelectTypeOption::from(field_rev);
|
||||
Ok(Box::new(type_option))
|
||||
}
|
||||
FieldType::MultiSelect => {
|
||||
let type_option = MultiSelectTypeOption::from(field_rev);
|
||||
Ok(Box::new(type_option))
|
||||
}
|
||||
ty => {
|
||||
tracing::error!("Unsupported field type: {:?} for this handler", ty);
|
||||
Err(ErrorCode::FieldInvalidOperation.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single select
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SingleSelectTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub disable_color: bool,
|
||||
}
|
||||
impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect);
|
||||
|
||||
impl SelectOptionOperation for SingleSelectTypeOption {
|
||||
fn select_option_cell_data(&self, cell_rev: &Option<CellRevision>) -> SelectOptionCellData {
|
||||
let select_options = make_select_context_from(cell_rev, &self.options);
|
||||
SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl CellFilterOperation<GridSelectOptionFilter, SelectOptionIds> for SingleSelectTypeOption {
|
||||
fn apply_filter(&self, _cell_data: SelectOptionIds, _filter: &GridSelectOptionFilter) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<String> for SingleSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
cell_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if !decoded_field_type.is_select_option() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let encoded_data = cell_data.into();
|
||||
let mut cell_data = SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options: vec![],
|
||||
};
|
||||
if let Some(option_id) = select_option_ids(encoded_data).first() {
|
||||
if let Some(option) = self.options.iter().find(|option| &option.id == option_id) {
|
||||
cell_data.select_options.push(option.clone());
|
||||
}
|
||||
}
|
||||
|
||||
DecodedCellData::try_from_bytes(cell_data)
|
||||
}
|
||||
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
|
||||
where
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let select_option_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset)?;
|
||||
let new_cell_data: String;
|
||||
if let Some(insert_option_id) = select_option_changeset.insert_option_id {
|
||||
tracing::trace!("Insert single select option: {}", &insert_option_id);
|
||||
new_cell_data = insert_option_id;
|
||||
} else {
|
||||
tracing::trace!("Delete single select option");
|
||||
new_cell_data = "".to_string()
|
||||
}
|
||||
|
||||
Ok(new_cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOption);
|
||||
impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOption);
|
||||
|
||||
impl SingleSelectTypeOptionBuilder {
|
||||
pub fn option(mut self, opt: SelectOption) -> Self {
|
||||
self.0.options.push(opt);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
FieldType::SingleSelect
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple select
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct MultiSelectTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub disable_color: bool,
|
||||
}
|
||||
impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect);
|
||||
|
||||
impl SelectOptionOperation for MultiSelectTypeOption {
|
||||
fn select_option_cell_data(&self, cell_rev: &Option<CellRevision>) -> SelectOptionCellData {
|
||||
let select_options = make_select_context_from(cell_rev, &self.options);
|
||||
SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
impl CellFilterOperation<GridSelectOptionFilter, SelectOptionIds> for MultiSelectTypeOption {
|
||||
fn apply_filter(&self, _cell_data: SelectOptionIds, _filter: &GridSelectOptionFilter) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl CellDataOperation<String> for MultiSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
cell_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if !decoded_field_type.is_select_option() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let encoded_data = cell_data.into();
|
||||
let select_options = select_option_ids(encoded_data)
|
||||
.into_iter()
|
||||
.flat_map(|option_id| self.options.iter().find(|option| option.id == option_id).cloned())
|
||||
.collect::<Vec<SelectOption>>();
|
||||
|
||||
let cell_data = SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
};
|
||||
|
||||
DecodedCellData::try_from_bytes(cell_data)
|
||||
}
|
||||
|
||||
fn apply_changeset<T>(&self, changeset: T, cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
|
||||
where
|
||||
T: Into<CellContentChangeset>,
|
||||
{
|
||||
let content_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset.into())?;
|
||||
let new_cell_data: String;
|
||||
match cell_rev {
|
||||
None => {
|
||||
new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned());
|
||||
}
|
||||
Some(cell_rev) => {
|
||||
let cell_data = get_cell_data(&cell_rev);
|
||||
let mut selected_options = select_option_ids(cell_data);
|
||||
if let Some(insert_option_id) = content_changeset.insert_option_id {
|
||||
tracing::trace!("Insert multi select option: {}", &insert_option_id);
|
||||
if selected_options.contains(&insert_option_id) {
|
||||
selected_options.retain(|id| id != &insert_option_id);
|
||||
} else {
|
||||
selected_options.push(insert_option_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delete_option_id) = content_changeset.delete_option_id {
|
||||
tracing::trace!("Delete multi select option: {}", &delete_option_id);
|
||||
selected_options.retain(|id| id != &delete_option_id);
|
||||
}
|
||||
|
||||
new_cell_data = selected_options.join(SELECTION_IDS_SEPARATOR);
|
||||
tracing::trace!("Multi select cell data: {}", &new_cell_data);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption);
|
||||
impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption);
|
||||
impl MultiSelectTypeOptionBuilder {
|
||||
pub fn option(mut self, opt: SelectOption) -> Self {
|
||||
self.0.options.push(opt);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
FieldType::MultiSelect
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectOptionIds(Vec<String>);
|
||||
impl std::convert::From<AnyCellData> for SelectOptionIds {
|
||||
fn from(any_cell_data: AnyCellData) -> Self {
|
||||
let ids = select_option_ids(any_cell_data.cell_data);
|
||||
Self(ids)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_option_ids(data: String) -> Vec<String> {
|
||||
data.split(SELECTION_IDS_SEPARATOR)
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SelectOption {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub color: SelectOptionColor,
|
||||
}
|
||||
|
||||
impl SelectOption {
|
||||
pub fn new(name: &str) -> Self {
|
||||
SelectOption {
|
||||
id: nanoid!(4),
|
||||
name: name.to_owned(),
|
||||
color: SelectOptionColor::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_color(name: &str, color: SelectOptionColor) -> Self {
|
||||
SelectOption {
|
||||
id: nanoid!(4),
|
||||
name: name.to_owned(),
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct SelectOptionChangesetPayload {
|
||||
#[pb(index = 1)]
|
||||
pub cell_identifier: CellIdentifierPayload,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub insert_option: Option<SelectOption>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub update_option: Option<SelectOption>,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub delete_option: Option<SelectOption>,
|
||||
}
|
||||
|
||||
pub struct SelectOptionChangeset {
|
||||
pub cell_identifier: CellIdentifier,
|
||||
pub insert_option: Option<SelectOption>,
|
||||
pub update_option: Option<SelectOption>,
|
||||
pub delete_option: Option<SelectOption>,
|
||||
}
|
||||
|
||||
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<SelectOptionChangeset, Self::Error> {
|
||||
let cell_identifier = self.cell_identifier.try_into()?;
|
||||
Ok(SelectOptionChangeset {
|
||||
cell_identifier,
|
||||
insert_option: self.insert_option,
|
||||
update_option: self.update_option,
|
||||
delete_option: self.delete_option,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct SelectOptionCellChangesetPayload {
|
||||
#[pb(index = 1)]
|
||||
pub cell_identifier: CellIdentifierPayload,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub insert_option_id: Option<String>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SelectOptionCellChangesetParams {
|
||||
pub cell_identifier: CellIdentifier,
|
||||
pub insert_option_id: Option<String>,
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
impl std::convert::From<SelectOptionCellChangesetParams> for CellChangeset {
|
||||
fn from(params: SelectOptionCellChangesetParams) -> Self {
|
||||
let changeset = SelectOptionCellContentChangeset {
|
||||
insert_option_id: params.insert_option_id,
|
||||
delete_option_id: params.delete_option_id,
|
||||
};
|
||||
let s = serde_json::to_string(&changeset).unwrap();
|
||||
CellChangeset {
|
||||
grid_id: params.cell_identifier.grid_id,
|
||||
row_id: params.cell_identifier.row_id,
|
||||
field_id: params.cell_identifier.field_id,
|
||||
cell_content_changeset: Some(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPayload {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
|
||||
let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?;
|
||||
let insert_option_id = match self.insert_option_id {
|
||||
None => None,
|
||||
Some(insert_option_id) => Some(
|
||||
NotEmptyStr::parse(insert_option_id)
|
||||
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
let delete_option_id = match self.delete_option_id {
|
||||
None => None,
|
||||
Some(delete_option_id) => Some(
|
||||
NotEmptyStr::parse(delete_option_id)
|
||||
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(SelectOptionCellChangesetParams {
|
||||
cell_identifier,
|
||||
insert_option_id,
|
||||
delete_option_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SelectOptionCellContentChangeset {
|
||||
pub insert_option_id: Option<String>,
|
||||
pub delete_option_id: Option<String>,
|
||||
}
|
||||
|
||||
impl SelectOptionCellContentChangeset {
|
||||
pub fn from_insert(option_id: &str) -> Self {
|
||||
SelectOptionCellContentChangeset {
|
||||
insert_option_id: Some(option_id.to_string()),
|
||||
delete_option_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_delete(option_id: &str) -> Self {
|
||||
SelectOptionCellContentChangeset {
|
||||
insert_option_id: None,
|
||||
delete_option_id: Some(option_id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SelectOptionCellData {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub select_options: Vec<SelectOption>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, 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,
|
||||
}
|
||||
|
||||
pub fn select_option_color_from_index(index: usize) -> SelectOptionColor {
|
||||
match index % 8 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for SelectOptionColor {
|
||||
fn default() -> Self {
|
||||
SelectOptionColor::Purple
|
||||
}
|
||||
}
|
||||
|
||||
fn make_select_context_from(cell_rev: &Option<CellRevision>, options: &[SelectOption]) -> Vec<SelectOption> {
|
||||
match cell_rev {
|
||||
None => vec![],
|
||||
Some(cell_rev) => {
|
||||
if let Ok(type_option_cell_data) = AnyCellData::from_str(&cell_rev.data) {
|
||||
select_option_ids(type_option_cell_data.cell_data)
|
||||
.into_iter()
|
||||
.flat_map(|option_id| options.iter().find(|option| option.id == option_id).cloned())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{
|
||||
MultiSelectTypeOption, MultiSelectTypeOptionBuilder, SelectOption, SelectOptionCellContentChangeset,
|
||||
SelectOptionCellData, SingleSelectTypeOption, SingleSelectTypeOptionBuilder, SELECTION_IDS_SEPARATOR,
|
||||
};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
||||
|
||||
#[test]
|
||||
fn single_select_test() {
|
||||
let google_option = SelectOption::new("Google");
|
||||
let facebook_option = SelectOption::new("Facebook");
|
||||
let twitter_option = SelectOption::new("Twitter");
|
||||
let single_select = SingleSelectTypeOptionBuilder::default()
|
||||
.option(google_option.clone())
|
||||
.option(facebook_option.clone())
|
||||
.option(twitter_option);
|
||||
|
||||
let field_rev = FieldBuilder::new(single_select)
|
||||
.name("Platform")
|
||||
.visibility(true)
|
||||
.build();
|
||||
|
||||
let type_option = SingleSelectTypeOption::from(&field_rev);
|
||||
|
||||
let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR);
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]);
|
||||
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None)
|
||||
.unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("123").to_str(), None)
|
||||
.unwrap();
|
||||
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid changeset
|
||||
assert!(type_option.apply_changeset("123", None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_select_test() {
|
||||
let google_option = SelectOption::new("Google");
|
||||
let facebook_option = SelectOption::new("Facebook");
|
||||
let twitter_option = SelectOption::new("Twitter");
|
||||
let multi_select = MultiSelectTypeOptionBuilder::default()
|
||||
.option(google_option.clone())
|
||||
.option(facebook_option.clone())
|
||||
.option(twitter_option);
|
||||
|
||||
let field_rev = FieldBuilder::new(multi_select)
|
||||
.name("Platform")
|
||||
.visibility(true)
|
||||
.build();
|
||||
|
||||
let type_option = MultiSelectTypeOption::from(&field_rev);
|
||||
|
||||
let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR);
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_multi_select_options(
|
||||
cell_data,
|
||||
&type_option,
|
||||
&field_rev,
|
||||
vec![google_option.clone(), facebook_option],
|
||||
);
|
||||
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None)
|
||||
.unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("123,456").to_str(), None)
|
||||
.unwrap();
|
||||
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid changeset
|
||||
assert!(type_option.apply_changeset("123", None).is_err());
|
||||
}
|
||||
|
||||
fn assert_multi_select_options(
|
||||
cell_data: String,
|
||||
type_option: &MultiSelectTypeOption,
|
||||
field_rev: &FieldRevision,
|
||||
expected: Vec<SelectOption>,
|
||||
) {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
assert_eq!(
|
||||
expected,
|
||||
type_option
|
||||
.decode_cell_data(cell_data, &field_type, field_rev)
|
||||
.unwrap()
|
||||
.parse::<SelectOptionCellData>()
|
||||
.unwrap()
|
||||
.select_options,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_single_select_options(
|
||||
cell_data: String,
|
||||
type_option: &SingleSelectTypeOption,
|
||||
field_rev: &FieldRevision,
|
||||
expected: Vec<SelectOption>,
|
||||
) {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
assert_eq!(
|
||||
expected,
|
||||
type_option
|
||||
.decode_cell_data(cell_data, &field_type, field_rev)
|
||||
.unwrap()
|
||||
.parse::<SelectOptionCellData>()
|
||||
.unwrap()
|
||||
.select_options,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
use crate::entities::{FieldType, GridSelectOptionFilter};
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::select_option::{
|
||||
make_selected_select_options, SelectOption, SelectOptionCellContentChangeset, SelectOptionCellData,
|
||||
SelectOptionIds, SelectOptionOperation,
|
||||
};
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{
|
||||
AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Single select
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct SingleSelectTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub options: Vec<SelectOption>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub disable_color: bool,
|
||||
}
|
||||
impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect);
|
||||
|
||||
impl SelectOptionOperation for SingleSelectTypeOption {
|
||||
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData {
|
||||
let select_options = make_selected_select_options(any_cell_data, &self.options);
|
||||
SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> &Vec<SelectOption> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_single_select() {
|
||||
return Ok(true);
|
||||
}
|
||||
let _ids: SelectOptionIds = any_cell_data.try_into()?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<String> for SingleSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
cell_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if !decoded_field_type.is_select_option() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let encoded_data = cell_data.into();
|
||||
let mut cell_data = SelectOptionCellData {
|
||||
options: self.options.clone(),
|
||||
select_options: vec![],
|
||||
};
|
||||
|
||||
let ids: SelectOptionIds = encoded_data.into();
|
||||
if let Some(option_id) = ids.first() {
|
||||
if let Some(option) = self.options.iter().find(|option| &option.id == option_id) {
|
||||
cell_data.select_options.push(option.clone());
|
||||
}
|
||||
}
|
||||
|
||||
DecodedCellData::try_from_bytes(cell_data)
|
||||
}
|
||||
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
|
||||
where
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let select_option_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset)?;
|
||||
let new_cell_data: String;
|
||||
if let Some(insert_option_id) = select_option_changeset.insert_option_id {
|
||||
tracing::trace!("Insert single select option: {}", &insert_option_id);
|
||||
new_cell_data = insert_option_id;
|
||||
} else {
|
||||
tracing::trace!("Delete single select option");
|
||||
new_cell_data = "".to_string()
|
||||
}
|
||||
|
||||
Ok(new_cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOption);
|
||||
impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOption);
|
||||
|
||||
impl SingleSelectTypeOptionBuilder {
|
||||
pub fn option(mut self, opt: SelectOption) -> Self {
|
||||
self.0.options.push(opt);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
FieldType::SingleSelect
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::type_options::*;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
||||
|
||||
#[test]
|
||||
fn single_select_test() {
|
||||
let google_option = SelectOption::new("Google");
|
||||
let facebook_option = SelectOption::new("Facebook");
|
||||
let twitter_option = SelectOption::new("Twitter");
|
||||
let single_select = SingleSelectTypeOptionBuilder::default()
|
||||
.option(google_option.clone())
|
||||
.option(facebook_option.clone())
|
||||
.option(twitter_option);
|
||||
|
||||
let field_rev = FieldBuilder::new(single_select)
|
||||
.name("Platform")
|
||||
.visibility(true)
|
||||
.build();
|
||||
|
||||
let type_option = SingleSelectTypeOption::from(&field_rev);
|
||||
|
||||
let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR);
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]);
|
||||
|
||||
let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str();
|
||||
let cell_data = type_option.apply_changeset(data, None).unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None)
|
||||
.unwrap();
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid option id
|
||||
let cell_data = type_option
|
||||
.apply_changeset(SelectOptionCellContentChangeset::from_insert("123").to_str(), None)
|
||||
.unwrap();
|
||||
|
||||
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
|
||||
|
||||
// Invalid changeset
|
||||
assert!(type_option.apply_changeset("123", None).is_err());
|
||||
}
|
||||
|
||||
fn assert_single_select_options(
|
||||
cell_data: String,
|
||||
type_option: &SingleSelectTypeOption,
|
||||
field_rev: &FieldRevision,
|
||||
expected: Vec<SelectOption>,
|
||||
) {
|
||||
let field_type: FieldType = field_rev.field_type_rev.into();
|
||||
assert_eq!(
|
||||
expected,
|
||||
type_option
|
||||
.decode_cell_data(cell_data, &field_type, field_rev)
|
||||
.unwrap()
|
||||
.parse::<SelectOptionCellData>()
|
||||
.unwrap()
|
||||
.select_options,
|
||||
);
|
||||
}
|
||||
}
|
@ -32,9 +32,14 @@ pub struct RichTextTypeOption {
|
||||
}
|
||||
impl_type_option!(RichTextTypeOption, FieldType::RichText);
|
||||
|
||||
impl CellFilterOperation<GridTextFilter, TextCellData> for RichTextTypeOption {
|
||||
fn apply_filter(&self, _cell_data: TextCellData, _filter: &GridTextFilter) -> bool {
|
||||
false
|
||||
impl CellFilterOperation<GridTextFilter> for RichTextTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_text() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let text_cell_data: TextCellData = any_cell_data.try_into()?;
|
||||
Ok(filter.apply(text_cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,16 +78,25 @@ impl CellDataOperation<String> for RichTextTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextCellData(String);
|
||||
impl std::convert::From<AnyCellData> for TextCellData {
|
||||
fn from(any_data: AnyCellData) -> Self {
|
||||
TextCellData(any_data.cell_data)
|
||||
pub struct TextCellData(pub String);
|
||||
impl AsRef<str> for TextCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<AnyCellData> for TextCellData {
|
||||
type Error = FlowyError;
|
||||
|
||||
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
|
||||
Ok(TextCellData(value.cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::*;
|
||||
use crate::services::row::CellDataOperation;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::entities::{FieldType, GridTextFilter};
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TextCellData, TypeOptionBuilder};
|
||||
use crate::services::row::{
|
||||
AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData, EncodedCellData,
|
||||
};
|
||||
@ -35,9 +35,14 @@ pub struct URLTypeOption {
|
||||
}
|
||||
impl_type_option!(URLTypeOption, FieldType::URL);
|
||||
|
||||
impl CellFilterOperation<GridTextFilter, URLCellData> for URLTypeOption {
|
||||
fn apply_filter(&self, _cell_data: URLCellData, _filter: &GridTextFilter) -> bool {
|
||||
false
|
||||
impl CellFilterOperation<GridTextFilter> for URLTypeOption {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult<bool> {
|
||||
if !any_cell_data.is_url() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let text_cell_data: TextCellData = any_cell_data.try_into()?;
|
||||
Ok(filter.apply(&text_cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,9 +126,17 @@ impl FromStr for URLCellData {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<AnyCellData> for URLCellData {
|
||||
fn from(any_cell_data: AnyCellData) -> Self {
|
||||
URLCellData::from_str(&any_cell_data.cell_data).unwrap_or_default()
|
||||
// impl std::convert::From<AnyCellData> for URLCellData {
|
||||
// fn from(any_cell_data: AnyCellData) -> Self {
|
||||
// URLCellData::from_str(&any_cell_data.cell_data).unwrap_or_default()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl std::convert::TryFrom<AnyCellData> for URLCellData {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
mod cell_data_util;
|
||||
|
||||
pub use crate::services::field::select_option::*;
|
||||
pub use cell_data_util::*;
|
@ -185,54 +185,61 @@ fn filter_cell(
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<RichTextTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data.into(), filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::Number => filter_cache.number_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<NumberTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data, filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::DateTime => filter_cache.date_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<DateTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data, filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::SingleSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<SingleSelectTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data.into(), filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::MultiSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<MultiSelectTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data.into(), filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::Checkbox => filter_cache.checkbox_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<CheckboxTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data.into(), filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
FieldType::URL => filter_cache.url_filter.get(&filter_id).and_then(|filter| {
|
||||
Some(
|
||||
field_rev
|
||||
.get_type_option_entry::<URLTypeOption>(field_type_rev)?
|
||||
.apply_filter(any_cell_data.into(), filter.value()),
|
||||
.apply_filter(any_cell_data, filter.value())
|
||||
.ok(),
|
||||
)
|
||||
}),
|
||||
}?;
|
||||
|
||||
let is_visible = !is_hidden;
|
||||
let is_visible = !is_hidden.unwrap_or(false);
|
||||
match filter_result.visible_by_field_id.get(&filter_id) {
|
||||
None => {
|
||||
if is_visible {
|
||||
|
@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub trait CellFilterOperation<T, C: From<AnyCellData>> {
|
||||
fn apply_filter(&self, cell_data: C, filter: &T) -> bool;
|
||||
pub trait CellFilterOperation<T> {
|
||||
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult<bool>;
|
||||
}
|
||||
|
||||
pub trait CellDataOperation<D> {
|
||||
@ -83,6 +83,25 @@ impl std::convert::TryFrom<&CellRevision> for AnyCellData {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<&Option<CellRevision>> for AnyCellData {
|
||||
type Error = FlowyError;
|
||||
|
||||
fn try_from(value: &Option<CellRevision>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
None => Err(FlowyError::invalid_data().context("Expected CellRevision, but receive None")),
|
||||
Some(cell_rev) => AnyCellData::try_from(cell_rev),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<Option<CellRevision>> for AnyCellData {
|
||||
type Error = FlowyError;
|
||||
|
||||
fn try_from(value: Option<CellRevision>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyCellData {
|
||||
pub fn new(content: String, field_type: FieldType) -> Self {
|
||||
AnyCellData {
|
||||
@ -119,6 +138,10 @@ impl AnyCellData {
|
||||
self.field_type == FieldType::MultiSelect
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::services::field::SelectOptionCellContentChangeset;
|
||||
use crate::services::field::select_option::SelectOptionCellContentChangeset;
|
||||
use crate::services::row::apply_cell_data_changeset;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
|
||||
|
@ -2,7 +2,8 @@ use crate::grid::field_util::make_date_cell_string;
|
||||
use crate::grid::script::EditorScript::*;
|
||||
use crate::grid::script::*;
|
||||
use flowy_grid::entities::{CellChangeset, FieldType};
|
||||
use flowy_grid::services::field::{MultiSelectTypeOption, SelectOptionCellContentChangeset, SingleSelectTypeOption};
|
||||
use flowy_grid::services::field::select_option::SelectOptionCellContentChangeset;
|
||||
use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_cell_update() {
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::grid::field_util::*;
|
||||
use crate::grid::script::EditorScript::*;
|
||||
use crate::grid::script::*;
|
||||
use flowy_grid::services::field::{SelectOption, SingleSelectTypeOption};
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::SingleSelectTypeOption;
|
||||
use flowy_grid_data_model::revision::TypeOptionDataEntry;
|
||||
use flowy_sync::entities::grid::FieldChangesetParams;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use flowy_grid::services::field::*;
|
||||
|
||||
use flowy_grid::entities::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::*;
|
||||
use flowy_grid_data_model::revision::*;
|
||||
|
||||
pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) {
|
||||
|
@ -4,9 +4,8 @@ use crate::grid::script::EditorScript::*;
|
||||
use crate::grid::script::*;
|
||||
use chrono::NaiveDateTime;
|
||||
use flowy_grid::entities::FieldType;
|
||||
use flowy_grid::services::field::{
|
||||
DateCellData, MultiSelectTypeOption, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR,
|
||||
};
|
||||
use flowy_grid::services::field::select_option::SELECTION_IDS_SEPARATOR;
|
||||
use flowy_grid::services::field::{DateCellData, MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
use flowy_grid::services::row::{decode_any_cell_data, CreateRowRevisionBuilder};
|
||||
use flowy_grid_data_model::revision::RowMetaChangeset;
|
||||
|
||||
|
@ -18,6 +18,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use strum::EnumCount;
|
||||
use tokio::time::sleep;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams};
|
||||
|
||||
pub enum EditorScript {
|
||||
|
Loading…
Reference in New Issue
Block a user