feat: checklist improve (#2653)

* feat: improve checklist

* feat: reimplement checklist

* test: fix
This commit is contained in:
Nathan.fooo 2023-05-30 09:41:33 +08:00 committed by GitHub
parent dc73df4203
commit 107662dceb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 897 additions and 501 deletions

View File

@ -1,3 +1,4 @@
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
@ -11,8 +12,7 @@ typedef CheckboxCellController = CellController<String, String>;
typedef NumberCellController = CellController<String, String>;
typedef SelectOptionCellController
= CellController<SelectOptionCellDataPB, String>;
typedef ChecklistCellController
= CellController<SelectOptionCellDataPB, String>;
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
typedef URLCellController = CellController<URLCellDataPB, String>;
@ -79,7 +79,6 @@ class CellControllerBuilder {
);
case FieldType.MultiSelect:
case FieldType.SingleSelect:
case FieldType.Checklist:
final cellDataLoader = CellDataLoader(
cellId: _cellId,
parser: SelectOptionCellDataParser(),
@ -93,6 +92,19 @@ class CellControllerBuilder {
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
);
case FieldType.Checklist:
final cellDataLoader = CellDataLoader(
cellId: _cellId,
parser: ChecklistCellDataParser(),
reloadOnFieldChanged: true,
);
return ChecklistCellController(
cellId: _cellId,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
);
case FieldType.URL:
final cellDataLoader = CellDataLoader(
cellId: _cellId,

View File

@ -84,6 +84,16 @@ class SelectOptionCellDataParser
}
}
class ChecklistCellDataParser implements CellDataParser<ChecklistCellDataPB> {
@override
ChecklistCellDataPB? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return ChecklistCellDataPB.fromBuffer(data);
}
}
class URLCellDataParser implements CellDataParser<URLCellDataPB> {
@override
URLCellDataPB? parserData(List<int> data) {

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';

View File

@ -0,0 +1,71 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
class ChecklistCellBackendService {
final CellIdentifier cellId;
ChecklistCellBackendService({required this.cellId});
Future<Either<Unit, FlowyError>> create({
required String name,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellId.viewId
..fieldId = cellId.fieldInfo.id
..rowId = cellId.rowId
..insertOptions.add(name);
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<Either<Unit, FlowyError>> delete({
required List<String> optionIds,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellId.viewId
..fieldId = cellId.fieldInfo.id
..rowId = cellId.rowId
..deleteOptionIds.addAll(optionIds);
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<Either<Unit, FlowyError>> select({
required String optionId,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellId.viewId
..fieldId = cellId.fieldInfo.id
..rowId = cellId.rowId
..selectedOptionIds.add(optionId);
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<Either<Unit, FlowyError>> update({
required SelectOptionPB option,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellId.viewId
..fieldId = cellId.fieldInfo.id
..rowId = cellId.rowId
..updateOptions.add(option);
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
final payload = CellIdPB.create()
..fieldId = cellId.fieldInfo.id
..viewId = cellId.viewId
..rowId = cellId.rowId
..rowId = cellId.rowId;
return DatabaseEventGetChecklistCellData(payload).send();
}
}

View File

@ -7,9 +7,9 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
class SelectOptionBackendService {
class SelectOptionCellBackendService {
final CellIdentifier cellId;
SelectOptionBackendService({required this.cellId});
SelectOptionCellBackendService({required this.cellId});
String get viewId => cellId.viewId;
String get fieldId => cellId.fieldInfo.id;

View File

@ -17,7 +17,7 @@ import 'field_type_option_editor.dart';
class FieldEditor extends StatefulWidget {
final String viewId;
final String fieldName;
final bool isGroupField;
final bool isGroupingField;
final Function(String)? onDeleted;
final IFieldTypeOptionLoader typeOptionLoader;
@ -25,7 +25,7 @@ class FieldEditor extends StatefulWidget {
required this.viewId,
this.fieldName = "",
required this.typeOptionLoader,
this.isGroupField = false,
this.isGroupingField = false,
this.onDeleted,
Key? key,
}) : super(key: key);
@ -61,7 +61,7 @@ class _FieldEditorState extends State<FieldEditor> {
create: (context) => FieldEditorBloc(
viewId: widget.viewId,
fieldName: widget.fieldName,
isGroupField: widget.isGroupField,
isGroupField: widget.isGroupingField,
loader: widget.typeOptionLoader,
)..add(const FieldEditorEvent.initial()),
child: ListView.builder(

View File

@ -226,7 +226,7 @@ class _SelectOptionColorCell extends StatelessWidget {
dimension: 16,
child: Container(
decoration: BoxDecoration(
color: color.make(context),
color: color.toColor(context),
shape: BoxShape.circle,
),
),

View File

@ -1,22 +1,22 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart';
import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'checklist_cell_editor_bloc.dart';
part 'checklist_cell_bloc.freezed.dart';
class ChecklistCardCellBloc
extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final ChecklistCellController cellController;
final SelectOptionBackendService _selectOptionSvc;
final ChecklistCellBackendService _checklistCellSvc;
void Function()? _onCellChangedFn;
ChecklistCardCellBloc({
required this.cellController,
}) : _selectOptionSvc =
SelectOptionBackendService(cellId: cellController.cellId),
}) : _checklistCellSvc =
ChecklistCellBackendService(cellId: cellController.cellId),
super(ChecklistCellState.initial(cellController)) {
on<ChecklistCellEvent>(
(event, emit) async {
@ -29,8 +29,8 @@ class ChecklistCardCellBloc
emit(
state.copyWith(
allOptions: data.options,
selectedOptions: data.selectOptions,
percent: percentFromSelectOptionCellData(data),
selectedOptions: data.selectedOptions,
percent: data.percentage,
),
);
},
@ -63,7 +63,7 @@ class ChecklistCardCellBloc
}
void _loadOptions() {
_selectOptionSvc.getCellData().then((result) {
_checklistCellSvc.getCellData().then((result) {
if (isClosed) return;
return result.fold(
@ -78,7 +78,7 @@ class ChecklistCardCellBloc
class ChecklistCellEvent with _$ChecklistCellEvent {
const factory ChecklistCellEvent.initial() = _InitialCell;
const factory ChecklistCellEvent.didReceiveOptions(
SelectOptionCellDataPB data,
ChecklistCellDataPB data,
) = _DidReceiveCellUpdate;
}

View File

@ -48,33 +48,33 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
final List<Widget> slivers = [
const SliverChecklistProgressBar(),
SliverToBoxAdapter(
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: state.allOptions.length,
itemBuilder: (BuildContext context, int index) {
return _ChecklistOptionCell(
option: state.allOptions[index],
popoverMutex: popoverMutex,
);
},
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
),
child: ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: state.allOptions.length,
itemBuilder: (BuildContext context, int index) {
return _ChecklistOptionCell(
option: state.allOptions[index],
popoverMutex: popoverMutex,
);
},
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
),
),
];
return ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: CustomScrollView(
shrinkWrap: true,
slivers: slivers,
controller: ScrollController(),
physics: StyledScrollPhysics(),
return Padding(
padding: const EdgeInsets.all(8.0),
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: CustomScrollView(
shrinkWrap: true,
slivers: slivers,
controller: ScrollController(),
physics: StyledScrollPhysics(),
),
),
);
},

View File

@ -1,7 +1,8 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart';
import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/log.dart';
@ -12,13 +13,13 @@ part 'checklist_cell_editor_bloc.freezed.dart';
class ChecklistCellEditorBloc
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
final SelectOptionBackendService _selectOptionService;
final ChecklistCellBackendService _checklistCellService;
final ChecklistCellController cellController;
ChecklistCellEditorBloc({
required this.cellController,
}) : _selectOptionService =
SelectOptionBackendService(cellId: cellController.cellId),
}) : _checklistCellService =
ChecklistCellBackendService(cellId: cellController.cellId),
super(ChecklistCellEditorState.initial(cellController)) {
on<ChecklistCellEditorEvent>(
(event, emit) async {
@ -31,7 +32,7 @@ class ChecklistCellEditorBloc
emit(
state.copyWith(
allOptions: _makeChecklistSelectOptions(data, state.predicate),
percent: percentFromSelectOptionCellData(data),
percent: data.percentage,
),
);
},
@ -50,11 +51,7 @@ class ChecklistCellEditorBloc
_updateOption(option);
},
selectOption: (option) async {
if (option.isSelected) {
await _selectOptionService.unSelect(optionIds: [option.data.id]);
} else {
await _selectOptionService.select(optionIds: [option.data.id]);
}
await _checklistCellService.select(optionId: option.data.id);
},
filterOption: (String predicate) {},
);
@ -69,20 +66,19 @@ class ChecklistCellEditorBloc
}
void _createOption(String name) async {
final result = await _selectOptionService.create(
name: name,
isSelected: false,
);
final result = await _checklistCellService.create(name: name);
result.fold((l) => {}, (err) => Log.error(err));
}
void _deleteOption(List<SelectOptionPB> options) async {
final result = await _selectOptionService.delete(options: options);
final result = await _checklistCellService.delete(
optionIds: options.map((e) => e.id).toList(),
);
result.fold((l) => null, (err) => Log.error(err));
}
void _updateOption(SelectOptionPB option) async {
final result = await _selectOptionService.update(
final result = await _checklistCellService.update(
option: option,
);
@ -90,7 +86,7 @@ class ChecklistCellEditorBloc
}
void _loadOptions() {
_selectOptionService.getCellData().then((result) {
_checklistCellService.getCellData().then((result) {
if (isClosed) return;
return result.fold(
@ -118,7 +114,7 @@ class ChecklistCellEditorBloc
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
const factory ChecklistCellEditorEvent.initial() = _Initial;
const factory ChecklistCellEditorEvent.didReceiveOptions(
SelectOptionCellDataPB data,
ChecklistCellDataPB data,
) = _DidReceiveOptions;
const factory ChecklistCellEditorEvent.newOption(String optionName) =
_NewOption;
@ -142,34 +138,20 @@ class ChecklistCellEditorState with _$ChecklistCellEditorState {
required String predicate,
}) = _ChecklistCellEditorState;
factory ChecklistCellEditorState.initial(SelectOptionCellController context) {
factory ChecklistCellEditorState.initial(ChecklistCellController context) {
final data = context.getCellData(loadIfNotExist: true);
return ChecklistCellEditorState(
allOptions: _makeChecklistSelectOptions(data, ''),
createOption: none(),
percent: percentFromSelectOptionCellData(data),
percent: data?.percentage ?? 0,
predicate: '',
);
}
}
double percentFromSelectOptionCellData(SelectOptionCellDataPB? data) {
if (data == null) return 0;
final b = data.options.length.toDouble();
if (b == 0) {
return 0;
}
final a = data.selectOptions.length.toDouble();
if (a > b) return 1.0;
return a / b;
}
List<ChecklistSelectOption> _makeChecklistSelectOptions(
SelectOptionCellDataPB? data,
ChecklistCellDataPB? data,
String predicate,
) {
if (data == null) {
@ -181,7 +163,7 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
if (predicate.isNotEmpty) {
allOptions.retainWhere((element) => element.name.contains(predicate));
}
final selectedOptionIds = data.selectOptions.map((e) => e.id).toList();
final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
for (final option in allOptions) {
options.add(

View File

@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -19,7 +21,9 @@ class ChecklistProgressBar extends StatelessWidget {
lineHeight: 10.0,
percent: percent,
padding: EdgeInsets.zero,
progressColor: Theme.of(context).colorScheme.primary,
progressColor: percent < 1.0
? SelectOptionColorPB.Blue.toColor(context)
: SelectOptionColorPB.Green.toColor(context),
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(5),
);

View File

@ -10,7 +10,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
extension SelectOptionColorExtension on SelectOptionColorPB {
Color make(BuildContext context) {
Color toColor(BuildContext context) {
switch (this) {
case SelectOptionColorPB.Purple:
return AFThemeExtension.of(context).tint1;
@ -82,7 +82,7 @@ class SelectOptionTag extends StatelessWidget {
}) {
return SelectOptionTag(
name: option.name,
color: option.color.make(context),
color: option.color.toColor(context),
onSelected: onSelected,
onRemove: onRemove,
);

View File

@ -5,19 +5,19 @@ import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'select_option_service.dart';
import '../../../../application/cell/select_option_cell_service.dart';
part 'select_option_editor_bloc.freezed.dart';
class SelectOptionCellEditorBloc
extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
final SelectOptionBackendService _selectOptionService;
final SelectOptionCellBackendService _selectOptionService;
final SelectOptionCellController cellController;
SelectOptionCellEditorBloc({
required this.cellController,
}) : _selectOptionService =
SelectOptionBackendService(cellId: cellController.cellId),
SelectOptionCellBackendService(cellId: cellController.cellId),
super(SelectOptionEditorState.initial(cellController)) {
on<SelectOptionEditorEvent>(
(event, emit) async {

View File

@ -309,7 +309,7 @@ class _PropertyCellState extends State<_PropertyCell> {
return FieldEditor(
viewId: widget.cellId.viewId,
fieldName: widget.cellId.fieldInfo.field.name,
isGroupField: widget.cellId.fieldInfo.isGroupField,
isGroupingField: widget.cellId.fieldInfo.isGroupField,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.cellId.viewId,
field: widget.cellId.fieldInfo.field,

View File

@ -73,7 +73,8 @@ class ColorOptionAction extends PopoverActionCell {
final transaction = editorState.transaction;
for (final node in nodes) {
transaction.updateNode(node, {
blockComponentBackgroundColor: color.make(context).toHex(),
blockComponentBackgroundColor:
color.toColor(context).toHex(),
});
}
editorState.apply(transaction);
@ -91,7 +92,7 @@ class ColorOptionAction extends PopoverActionCell {
return null;
}
for (final value in SelectOptionColorPB.values) {
if (value.make(context).toHex() == hexColor) {
if (value.toColor(context).toHex() == hexColor) {
return value;
}
}

View File

@ -22,10 +22,9 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
switch (field.field_type) {
case FieldType.SingleSelect:
case FieldType.MultiSelect:
case FieldType.Checklist: {
case FieldType.MultiSelect: {
let selectOptions: ISelectOption[] = [];
let typeOption: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB | ChecklistTypeOptionPB | undefined;
let typeOption: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB | undefined;
if (field.field_type === FieldType.SingleSelect) {
typeOption = (await makeSingleSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
@ -39,9 +38,6 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
if (field.field_type === FieldType.MultiSelect) {
typeOption = (await makeMultiSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
}
if (field.field_type === FieldType.Checklist) {
typeOption = (await makeChecklistTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
}
if (typeOption) {
selectOptions = typeOption.options.map<ISelectOption>((option) => {

View File

@ -592,7 +592,7 @@ impl FieldType {
self == &MULTI_SELECT_FIELD || self == &SINGLE_SELECT_FIELD
}
pub fn is_check_list(&self) -> bool {
pub fn is_checklist(&self) -> bool {
self == &CHECKLIST_FIELD
}

View File

@ -0,0 +1,98 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::SelectOptionPB;
use crate::services::field::checklist_type_option::ChecklistCellData;
use crate::services::field::SelectOption;
use collab_database::rows::RowId;
use flowy_derive::ProtoBuf;
use flowy_error::{ErrorCode, FlowyError};
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChecklistCellDataPB {
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
#[pb(index = 2)]
pub selected_options: Vec<SelectOptionPB>,
#[pb(index = 3)]
pub(crate) percentage: f64,
}
impl From<ChecklistCellData> for ChecklistCellDataPB {
fn from(cell_data: ChecklistCellData) -> Self {
let selected_options = cell_data.selected_options();
let percentage = cell_data.percentage_complete();
Self {
options: cell_data
.options
.into_iter()
.map(|option| option.into())
.collect(),
selected_options: selected_options
.into_iter()
.map(|option| option.into())
.collect(),
percentage,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChecklistCellDataChangesetPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub row_id: String,
#[pb(index = 4)]
pub insert_options: Vec<String>,
#[pb(index = 5)]
pub selected_option_ids: Vec<String>,
#[pb(index = 6)]
pub delete_option_ids: Vec<String>,
#[pb(index = 7)]
pub update_options: Vec<SelectOptionPB>,
}
#[derive(Debug)]
pub struct ChecklistCellDataChangesetParams {
pub view_id: String,
pub field_id: String,
pub row_id: RowId,
pub insert_options: Vec<String>,
pub selected_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
pub update_options: Vec<SelectOption>,
}
impl TryInto<ChecklistCellDataChangesetParams> for ChecklistCellDataChangesetPB {
type Error = FlowyError;
fn try_into(self) -> Result<ChecklistCellDataChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
Ok(ChecklistCellDataChangesetParams {
view_id: view_id.0,
field_id: field_id.0,
row_id: RowId::from(row_id.0),
insert_options: self.insert_options,
selected_option_ids: self.selected_option_ids,
delete_option_ids: self.delete_option_ids,
update_options: self
.update_options
.into_iter()
.map(SelectOption::from)
.collect(),
})
}
}

View File

@ -1,4 +1,5 @@
mod checkbox_entities;
mod checklist_entities;
mod date_entities;
mod number_entities;
mod select_option;
@ -6,6 +7,7 @@ mod text_entities;
mod url_entities;
pub use checkbox_entities::*;
pub use checklist_entities::*;
pub use date_entities::*;
pub use number_entities::*;
pub use select_option::*;

View File

@ -1,8 +1,8 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::{CellIdPB, CellIdParams};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
ChecklistTypeOption, MultiSelectTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption,
MultiSelectTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption,
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
@ -287,34 +287,19 @@ impl From<MultiSelectTypeOptionPB> for MultiSelectTypeOption {
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct ChecklistTypeOptionPB {
#[pb(index = 1)]
pub options: Vec<SelectOptionPB>,
#[pb(index = 2)]
pub disable_color: bool,
pub config: String,
}
impl From<ChecklistTypeOption> for ChecklistTypeOptionPB {
fn from(data: ChecklistTypeOption) -> Self {
fn from(_data: ChecklistTypeOption) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
config: "".to_string(),
}
}
}
impl From<ChecklistTypeOptionPB> for ChecklistTypeOption {
fn from(data: ChecklistTypeOptionPB) -> Self {
Self {
options: data
.options
.into_iter()
.map(|option| option.into())
.collect(),
disable_color: data.disable_color,
}
fn from(_data: ChecklistTypeOptionPB) -> Self {
Self
}
}

View File

@ -12,6 +12,7 @@ use crate::entities::*;
use crate::manager::DatabaseManager2;
use crate::services::cell::CellBuilder;
use crate::services::field::checklist_type_option::ChecklistCellChangeset;
use crate::services::field::{
type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset,
};
@ -469,6 +470,38 @@ pub(crate) async fn update_select_option_cell_handler(
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_checklist_cell_data_handler(
data: AFPluginData<CellIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> DataResult<ChecklistCellDataPB, FlowyError> {
let params: CellIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let data = database_editor
.get_checklist_option(params.row_id, &params.field_id)
.await;
data_result_ok(data)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_checklist_cell_handler(
data: AFPluginData<ChecklistCellDataChangesetPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> Result<(), FlowyError> {
let params: ChecklistCellDataChangesetParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let changeset = ChecklistCellChangeset {
insert_options: params.insert_options,
selected_option_ids: params.selected_option_ids,
delete_option_ids: params.delete_option_ids,
update_options: params.update_options,
};
database_editor
.set_checklist_options(&params.view_id, params.row_id, &params.field_id, changeset)
.await?;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_date_cell_handler(
data: AFPluginData<DateChangesetPB>,

View File

@ -44,6 +44,9 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
.event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler)
.event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler)
.event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler)
// Checklist
.event(DatabaseEvent::GetChecklistCellData, get_checklist_cell_data_handler)
.event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler)
// Date
.event(DatabaseEvent::UpdateDateCell, update_date_cell_handler)
// Group
@ -227,6 +230,12 @@ pub enum DatabaseEvent {
#[event(input = "SelectOptionCellChangesetPB")]
UpdateSelectOptionCell = 72,
#[event(input = "CellIdPB", output = "ChecklistCellDataPB")]
GetChecklistCellData = 73,
#[event(input = "ChecklistCellDataChangesetPB")]
UpdateChecklistCell = 74,
/// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB]
/// contains the date and the time string. It can be cast to [CellChangesetPB] that
/// will be used by the `update_cell` function.

View File

@ -9,43 +9,47 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellProtobufBlob};
use crate::services::field::checklist_type_option::ChecklistCellChangeset;
use crate::services::field::*;
use crate::services::group::make_no_status_group;
/// Decode the opaque cell data into readable format content
pub trait CellDataDecoder: TypeOption {
///
/// Tries to decode the opaque cell string to `decoded_field_type`'s cell data. Sometimes, the `field_type`
/// of the `FieldRevision` is not equal to the `decoded_field_type`(This happened When switching
/// the field type of the `FieldRevision` to another field type). So the cell data is need to do
/// Tries to decode the [Cell] to `decoded_field_type`'s cell data. Sometimes, the `field_type`
/// of the `Field` is not equal to the `decoded_field_type`(This happened When switching
/// the field type of the `Field` to another field type). So the cell data is need to do
/// some transformation.
///
/// For example, the current field type of the `FieldRevision` is a checkbox. When switching the field
/// For example, the current field type of the `Field` is a checkbox. When switching the field
/// type from the checkbox to single select, it will create two new options,`Yes` and `No`, if they don't exist.
/// But the data of the cell doesn't change. We can't iterate all the rows to transform the cell
/// data that can be parsed by the current field type. One approach is to transform the cell data
/// when it get read. For the moment, the cell data is a string, `Yes` or `No`. It needs to compare
/// with the option's name, if match return the id of the option.
fn decode_cell_str(
/// when reading.
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData>;
/// Same as `decode_cell_data` does but Decode the cell data to readable `String`
/// Decode the cell data to readable `String`
/// For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma.
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String;
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String;
fn decode_cell_to_str(&self, cell: &Cell) -> String;
/// Same as [CellDataDecoder::stringify_cell_data] but the input parameter is the [Cell]
fn stringify_cell(&self, cell: &Cell) -> String;
}
pub trait CellDataChangeset: TypeOption {
/// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset`
/// implements the `FromCellChangesetString` trait.
/// For example,the SelectOptionCellChangeset,DateCellChangeset. etc.
/// # Arguments
///
/// * `changeset`: the cell changeset that represents the changes of the cell.
/// * `cell`: the data of the cell. It will be None if the cell does not contain any data.
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
@ -109,13 +113,12 @@ pub fn get_cell_protobuf(
///
/// # Arguments
///
/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the
/// `FromCellString` trait.
/// * `cell`: the opaque cell string that can be decoded by corresponding structs.
/// * `from_field_type`: the original field type of the passed-in cell data. Check the `TypeCellData`
/// that is used to save the origin field type of the cell data.
/// * `to_field_type`: decode the passed-in cell data to this field type. It will use the to_field_type's
/// TypeOption to decode this cell data.
/// * `field_rev`: used to get the corresponding TypeOption for the specified field type.
/// * `field`: used to get the corresponding TypeOption for the specified field type.
///
/// returns: CellBytes
///
@ -154,11 +157,10 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
///
/// # Arguments
///
/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the
/// `FromCellString` trait.
/// * `cell`: the opaque cell string that can be decoded by corresponding structs
/// * `to_field_type`: the cell will be decoded to this field type's cell data.
/// * `from_field_type`: the original field type of the passed-in cell data.
/// * `field_rev`: used to get the corresponding TypeOption for the specified field type.
/// * `field`: used to get the corresponding TypeOption for the specified field type.
///
/// returns: String
pub fn stringify_cell_data(
@ -223,6 +225,15 @@ pub fn insert_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell
apply_cell_changeset(changeset, None, field, None).unwrap()
}
pub fn insert_checklist_cell(insert_options: Vec<String>, field: &Field) -> Cell {
let changeset = ChecklistCellChangeset {
insert_options,
..Default::default()
}
.to_cell_changeset_str();
apply_cell_changeset(changeset, None, field, None).unwrap()
}
pub fn delete_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
let changeset =
SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str();
@ -434,4 +445,15 @@ impl<'a> CellBuilder<'a> {
},
}
}
pub fn insert_checklist_cell(&mut self, field_id: &str, option_names: Vec<String>) {
match self.field_maps.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the field with id: {}", field_id),
Some(field) => {
self.cells.insert(
field_id.to_owned(),
insert_checklist_cell(option_names, field),
);
},
}
}
}

View File

@ -15,8 +15,8 @@ use flowy_task::TaskDispatcher;
use lib_infra::future::{to_fut, Fut};
use crate::entities::{
CalendarEventPB, CellChangesetNotifyPB, CellPB, DatabaseFieldChangesetPB, DatabasePB,
DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams,
CalendarEventPB, CellChangesetNotifyPB, CellPB, ChecklistCellDataPB, DatabaseFieldChangesetPB,
DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams,
FieldChangesetParams, FieldIdPB, FieldPB, FieldType, GroupPB, IndexFieldPB, InsertedRowPB,
LayoutSettingParams, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB, RowPB, RowsChangePB,
SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams, UpdateSortParams,
@ -28,6 +28,7 @@ use crate::services::cell::{
};
use crate::services::database::util::database_view_setting_pb_from_view;
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
use crate::services::field::{
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset,
@ -552,6 +553,7 @@ impl DatabaseEditor {
Ok(())
}
/// Just create an option for the field's type option. The option is save to the database.
pub async fn create_select_option(
&self,
field_id: &str,
@ -652,6 +654,33 @@ impl DatabaseEditor {
}
}
pub async fn get_checklist_option(&self, row_id: RowId, field_id: &str) -> ChecklistCellDataPB {
let row_cell = self.database.lock().get_cell(field_id, &row_id);
let cell_data = match row_cell {
None => ChecklistCellData::default(),
Some(row_cell) => ChecklistCellData::from(&row_cell.cell),
};
ChecklistCellDataPB::from(cell_data)
}
pub async fn set_checklist_options(
&self,
view_id: &str,
row_id: RowId,
field_id: &str,
changeset: ChecklistCellChangeset,
) -> FlowyResult<()> {
let field = self.database.lock().fields.get_field(field_id).ok_or(
FlowyError::record_not_found().context(format!("Field with id:{} not found", &field_id)),
)?;
debug_assert!(FieldType::from(field.field_type).is_checklist());
self
.update_cell_with_changeset(view_id, row_id, field_id, changeset)
.await?;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn load_groups(&self, view_id: &str) -> FlowyResult<RepeatedGroupPB> {
let view = self.database_views.get_view_editor(view_id).await?;

View File

@ -41,7 +41,7 @@ mod tests {
) {
assert_eq!(
type_option
.decode_cell_str(
.decode_cell(
&CheckboxCellData::from_cell_str(input_str).unwrap().into(),
field_type,
field

View File

@ -67,20 +67,20 @@ impl From<CheckboxTypeOption> for TypeOptionData {
}
impl TypeOptionCellData for CheckboxTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
cell_data
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(CheckboxCellData::from(cell))
}
}
impl CellDataDecoder for CheckboxTypeOption {
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
@ -90,14 +90,14 @@ impl CellDataDecoder for CheckboxTypeOption {
return Ok(Default::default());
}
self.decode_cell(cell)
self.parse_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string()
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
}

View File

@ -0,0 +1,207 @@
use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, FieldType, SelectOptionPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
use crate::services::field::{
SelectOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use std::cmp::Ordering;
#[derive(Debug, Clone, Default)]
pub struct ChecklistTypeOption;
impl TypeOption for ChecklistTypeOption {
type CellData = ChecklistCellData;
type CellChangeset = ChecklistCellChangeset;
type CellProtobufType = ChecklistCellDataPB;
type CellFilter = ChecklistFilterPB;
}
impl From<TypeOptionData> for ChecklistTypeOption {
fn from(_data: TypeOptionData) -> Self {
Self
}
}
impl From<ChecklistTypeOption> for TypeOptionData {
fn from(_data: ChecklistTypeOption) -> Self {
TypeOptionDataBuilder::new().build()
}
}
impl TypeOptionCellData for ChecklistTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
let percentage = cell_data.percentage_complete();
let selected_options = cell_data
.options
.iter()
.filter(|option| cell_data.selected_option_ids.contains(&option.id))
.map(|option| SelectOptionPB::from(option.clone()))
.collect();
let options = cell_data
.options
.into_iter()
.map(SelectOptionPB::from)
.collect();
ChecklistCellDataPB {
options,
selected_options,
percentage,
}
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(ChecklistCellData::from(cell))
}
}
impl CellDataChangeset for ChecklistTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
match cell {
Some(cell) => {
let mut cell_data = self.parse_cell(&cell)?;
update_cell_data_with_changeset(&mut cell_data, changeset);
Ok((Cell::from(cell_data.clone()), cell_data))
},
None => {
let cell_data = ChecklistCellData::from_options(changeset.insert_options);
Ok((Cell::from(cell_data.clone()), cell_data))
},
}
}
}
#[inline]
fn update_cell_data_with_changeset(
cell_data: &mut ChecklistCellData,
mut changeset: ChecklistCellChangeset,
) {
// Delete the options
cell_data
.options
.retain(|option| !changeset.delete_option_ids.contains(&option.id));
cell_data
.selected_option_ids
.retain(|option_id| !changeset.delete_option_ids.contains(option_id));
// Insert new options
changeset.insert_options.retain(|option_name| {
!cell_data
.options
.iter()
.any(|option| option.name == *option_name)
});
changeset
.insert_options
.into_iter()
.for_each(|option_name| {
let option = SelectOption::new(&option_name);
cell_data.options.push(option.clone());
});
// Update options
changeset
.update_options
.into_iter()
.for_each(|updated_option| {
if let Some(option) = cell_data
.options
.iter_mut()
.find(|option| option.id == updated_option.id)
{
option.name = updated_option.name;
}
});
// Select the options
changeset
.selected_option_ids
.into_iter()
.for_each(|option_id| {
if let Some(index) = cell_data
.selected_option_ids
.iter()
.position(|id| **id == option_id)
{
cell_data.selected_option_ids.remove(index);
} else {
cell_data.selected_option_ids.push(option_id);
}
});
}
impl CellDataDecoder for ChecklistTypeOption {
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
if !decoded_field_type.is_checklist() {
return Ok(Default::default());
}
self.parse_cell(cell)
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data
.selected_options()
.into_iter()
.map(|option| option.name)
.collect::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR)
}
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = self.parse_cell(cell).unwrap_or_default();
self.stringify_cell_data(cell_data)
}
}
impl TypeOptionCellDataFilter for ChecklistTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_checklist() {
return true;
}
let selected_options = cell_data.selected_options();
filter.is_visible(&cell_data.options, &selected_options)
}
}
impl TypeOptionCellDataCompare for ChecklistTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
let left = cell_data.percentage_complete();
let right = other_cell_data.percentage_complete();
if left > right {
Ordering::Greater
} else if left < right {
Ordering::Less
} else {
Ordering::Equal
}
}
}
impl TypeOptionTransform for ChecklistTypeOption {}

View File

@ -0,0 +1,107 @@
use crate::entities::FieldType;
use crate::services::cell::{FromCellChangeset, ToCellChangeset};
use crate::services::field::{SelectOption, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use flowy_error::{internal_error, FlowyResult};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct ChecklistCellData {
pub options: Vec<SelectOption>,
pub selected_option_ids: Vec<String>,
}
impl ToString for ChecklistCellData {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
}
impl ChecklistCellData {
pub fn selected_options(&self) -> Vec<SelectOption> {
self
.options
.iter()
.filter(|option| self.selected_option_ids.contains(&option.id))
.cloned()
.collect()
}
pub fn percentage_complete(&self) -> f64 {
let selected_options = self.selected_option_ids.len();
let total_options = self.options.len();
if total_options == 0 {
return 0.0;
}
(selected_options as f64) / (total_options as f64)
}
pub fn from_options(options: Vec<String>) -> Self {
let options = options
.into_iter()
.map(|option_name| SelectOption::new(&option_name))
.collect();
Self {
options,
..Default::default()
}
}
}
impl From<&Cell> for ChecklistCellData {
fn from(cell: &Cell) -> Self {
cell
.get_str_value(CELL_DATA)
.map(|data| serde_json::from_str::<ChecklistCellData>(&data).unwrap_or_default())
.unwrap_or_default()
}
}
impl From<ChecklistCellData> for Cell {
fn from(cell_data: ChecklistCellData) -> Self {
let data = serde_json::to_string(&cell_data).unwrap_or_default();
new_cell_builder(FieldType::Checklist)
.insert_str_value(CELL_DATA, data)
.build()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChecklistCellChangeset {
/// List of option names that will be inserted
pub insert_options: Vec<String>,
pub selected_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
pub update_options: Vec<SelectOption>,
}
impl FromCellChangeset for ChecklistCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<ChecklistCellChangeset>(&changeset).map_err(internal_error)
}
}
impl ToCellChangeset for ChecklistCellChangeset {
fn to_cell_changeset_str(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test() {
let a = 1;
let b = 2;
let c = (a as f32) / (b as f32);
println!("{}", c);
}
}

View File

@ -0,0 +1,5 @@
mod checklist;
mod checklist_entities;
pub use checklist::*;
pub use checklist_entities::*;

View File

@ -506,9 +506,9 @@ mod tests {
field: &Field,
) -> String {
let decoded_data = type_option
.decode_cell_str(cell, &FieldType::DateTime, field)
.decode_cell(cell, &FieldType::DateTime, field)
.unwrap();
let decoded_data = type_option.convert_to_protobuf(decoded_data);
let decoded_data = type_option.protobuf_encode(decoded_data);
if include_time {
format!("{} {}", decoded_data.date, decoded_data.time)
.trim_end()

View File

@ -66,14 +66,14 @@ impl From<DateTypeOption> for TypeOptionData {
}
impl TypeOptionCellData for DateTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.today_desc_from_timestamp(cell_data)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(DateCellData::from(cell))
}
}
@ -156,7 +156,7 @@ impl DateTypeOption {
impl TypeOptionTransform for DateTypeOption {}
impl CellDataDecoder for DateTypeOption {
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
@ -170,16 +170,16 @@ impl CellDataDecoder for DateTypeOption {
return Ok(Default::default());
}
self.decode_cell(cell)
self.parse_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
self.today_desc_from_timestamp(cell_data).date
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
self.stringify_cell_data(cell_data)
}
}

View File

@ -1,4 +1,5 @@
pub mod checkbox_type_option;
pub mod checklist_type_option;
pub mod date_type_option;
pub mod number_type_option;
pub mod selection_type_option;

View File

@ -89,7 +89,7 @@ mod tests {
) {
assert_eq!(
type_option
.decode_cell_str(
.decode_cell(
&NumberCellData(input_str.to_owned()).into(),
field_type,
field

View File

@ -96,14 +96,14 @@ impl From<NumberTypeOption> for TypeOptionData {
}
impl TypeOptionCellData for NumberTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(NumberCellData::from(cell))
}
}
@ -171,7 +171,7 @@ impl NumberTypeOption {
impl TypeOptionTransform for NumberTypeOption {}
impl CellDataDecoder for NumberTypeOption {
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
@ -181,22 +181,22 @@ impl CellDataDecoder for NumberTypeOption {
return Ok(Default::default());
}
let num_cell_data = self.decode_cell(cell)?;
let num_cell_data = self.parse_cell(cell)?;
Ok(NumberCellData::from(
self.format_cell_data(&num_cell_data)?.to_string(),
))
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
match self.format_cell_data(&cell_data) {
Ok(cell_data) => cell_data.to_string(),
Err(_) => "".to_string(),
}
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
self.stringify_cell_data(cell_data)
}
}

View File

@ -1,14 +1,13 @@
use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB};
use crate::services::field::{SelectOption, SelectedSelectOptions};
use crate::services::field::SelectOption;
impl ChecklistFilterPB {
pub fn is_visible(
&self,
all_options: &[SelectOption],
selected_options: &SelectedSelectOptions,
selected_options: &[SelectOption],
) -> bool {
let selected_option_ids = selected_options
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<&str>>();

View File

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

View File

@ -1,5 +1,4 @@
mod checklist_filter;
mod checklist_type_option;
mod multi_select_type_option;
mod select_filter;
mod select_ids;
@ -9,7 +8,6 @@ mod single_select_type_option;
mod type_option_transform;
pub use checklist_filter::*;
pub use checklist_type_option::*;
pub use multi_select_type_option::*;
pub use select_ids::*;
pub use select_option::*;

View File

@ -11,8 +11,8 @@ use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, SelectOptionCellChangeset, SelectOptionIds,
SelectTypeOptionSharedAction, SelectedSelectOptions, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter,
SelectTypeOptionSharedAction, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter,
};
// Multiple select
@ -48,14 +48,14 @@ impl From<MultiSelectTypeOption> for TypeOptionData {
}
impl TypeOptionCellData for MultiSelectTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.get_selected_options(cell_data).into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SelectOptionIds::from(cell))
}
}
@ -130,8 +130,7 @@ impl TypeOptionCellDataFilter for MultiSelectTypeOption {
if !field_type.is_multi_select() {
return true;
}
let selected_options =
SelectedSelectOptions::from(self.get_selected_options(cell_data.clone()));
let selected_options = self.get_selected_options(cell_data.clone()).select_options;
filter.is_visible(&selected_options, FieldType::MultiSelect)
}
}

View File

@ -1,19 +1,12 @@
#![allow(clippy::needless_collect)]
use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use crate::services::field::SelectedSelectOptions;
use crate::services::field::SelectOption;
impl SelectOptionFilterPB {
pub fn is_visible(
&self,
selected_options: &SelectedSelectOptions,
field_type: FieldType,
) -> bool {
let selected_option_ids: Vec<&String> = selected_options
.options
.iter()
.map(|option| &option.id)
.collect();
pub fn is_visible(&self, selected_options: &[SelectOption], field_type: FieldType) -> bool {
let selected_option_ids: Vec<&String> =
selected_options.iter().map(|option| &option.id).collect();
match self.condition {
SelectOptionConditionPB::OptionIs => match field_type {
FieldType::SingleSelect => {
@ -21,7 +14,7 @@ impl SelectOptionFilterPB {
return true;
}
if selected_options.options.is_empty() {
if selected_options.is_empty() {
return false;
}
@ -54,7 +47,7 @@ impl SelectOptionFilterPB {
return true;
}
if selected_options.options.is_empty() {
if selected_options.is_empty() {
return false;
}
@ -87,7 +80,6 @@ impl SelectOptionFilterPB {
mod tests {
#![allow(clippy::all)]
use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use crate::services::field::selection_type_option::SelectedSelectOptions;
use crate::services::field::SelectOption;
#[test]
@ -98,37 +90,15 @@ mod tests {
option_ids: vec![],
};
assert_eq!(filter.is_visible(&vec![], FieldType::SingleSelect), true);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::SingleSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option.clone()]
},
FieldType::SingleSelect
),
filter.is_visible(&vec![option.clone()], FieldType::SingleSelect),
false,
);
assert_eq!(filter.is_visible(&vec![], FieldType::MultiSelect), true);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::MultiSelect
),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option]
},
FieldType::MultiSelect
),
filter.is_visible(&vec![option], FieldType::MultiSelect),
false,
);
}
@ -143,38 +113,16 @@ mod tests {
};
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option_1.clone()]
},
FieldType::SingleSelect
),
filter.is_visible(&vec![option_1.clone()], FieldType::SingleSelect),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::SingleSelect
),
false,
);
assert_eq!(filter.is_visible(&vec![], FieldType::SingleSelect), false,);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option_1.clone()]
},
FieldType::MultiSelect
),
filter.is_visible(&vec![option_1.clone()], FieldType::MultiSelect),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions { options: vec![] },
FieldType::MultiSelect
),
false,
);
assert_eq!(filter.is_visible(&vec![], FieldType::MultiSelect), false,);
}
#[test]
@ -194,7 +142,7 @@ mod tests {
(vec![option_1.clone(), option_2.clone()], false),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
filter.is_visible(&options, FieldType::SingleSelect),
is_visible
);
}
@ -217,7 +165,7 @@ mod tests {
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
filter.is_visible(&options, FieldType::SingleSelect),
is_visible
);
}
@ -238,7 +186,7 @@ mod tests {
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
filter.is_visible(&options, FieldType::SingleSelect),
is_visible
);
}
@ -266,7 +214,7 @@ mod tests {
(vec![], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
filter.is_visible(&options, FieldType::MultiSelect),
is_visible
);
}
@ -292,7 +240,7 @@ mod tests {
(vec![option_3.clone()], false),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
filter.is_visible(&options, FieldType::MultiSelect),
is_visible
);
}
@ -308,7 +256,7 @@ mod tests {
};
for (options, is_visible) in vec![(vec![option_1.clone()], true), (vec![], true)] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
filter.is_visible(&options, FieldType::MultiSelect),
is_visible
);
}

View File

@ -83,15 +83,3 @@ pub fn make_selected_options(ids: SelectOptionIds, options: &[SelectOption]) ->
})
.collect()
}
pub struct SelectedSelectOptions {
pub(crate) options: Vec<SelectOption>,
}
impl std::convert::From<SelectOptionCellData> for SelectedSelectOptions {
fn from(data: SelectOptionCellData) -> Self {
Self {
options: data.select_options,
}
}
}

View File

@ -9,11 +9,12 @@ use crate::entities::{FieldType, SelectOptionCellDataPB};
use crate::services::cell::{
CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellChangeset, ToCellChangeset,
};
use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper;
use crate::services::field::{
make_selected_options, CheckboxCellData, ChecklistTypeOption, MultiSelectTypeOption,
SelectOption, SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption,
TypeOption, TypeOptionCellData, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
make_selected_options, CheckboxCellData, MultiSelectTypeOption, SelectOption,
SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, TypeOption,
TypeOptionCellData, TypeOptionTransform, SELECTION_IDS_SEPARATOR,
};
/// Defines the shared actions used by SingleSelect or Multi-Select.
@ -124,16 +125,16 @@ impl<T> CellDataDecoder for T
where
T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellData,
{
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
self.decode_cell(cell)
self.parse_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
self
.get_selected_options(cell_data)
.select_options
@ -143,35 +144,29 @@ where
.join(SELECTION_IDS_SEPARATOR)
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
self.stringify_cell_data(cell_data)
}
}
pub fn select_type_option_from_field(
field_rev: &Field,
field: &Field,
) -> FlowyResult<Box<dyn SelectTypeOptionSharedAction>> {
let field_type = FieldType::from(field_rev.field_type);
let field_type = FieldType::from(field.field_type);
match &field_type {
FieldType::SingleSelect => {
let type_option = field_rev
let type_option = field
.get_type_option::<SingleSelectTypeOption>(field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
FieldType::MultiSelect => {
let type_option = field_rev
let type_option = field
.get_type_option::<MultiSelectTypeOption>(&field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
FieldType::Checklist => {
let type_option = field_rev
.get_type_option::<ChecklistTypeOption>(&field_type)
.unwrap_or_default();
Ok(Box::new(type_option))
},
ty => {
tracing::error!("Unsupported field type: {:?} for this handler", ty);
Err(ErrorCode::FieldInvalidOperation.into())

View File

@ -1,8 +1,8 @@
use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB};
use crate::services::cell::CellDataChangeset;
use crate::services::field::{
default_order, SelectOption, SelectedSelectOptions, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter,
default_order, SelectOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter,
};
use crate::services::field::{
SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction,
@ -47,14 +47,14 @@ impl From<SingleSelectTypeOption> for TypeOptionData {
}
impl TypeOptionCellData for SingleSelectTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.get_selected_options(cell_data).into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SelectOptionIds::from(cell))
}
}
@ -121,8 +121,7 @@ impl TypeOptionCellDataFilter for SingleSelectTypeOption {
if !field_type.is_single_select() {
return true;
}
let selected_options =
SelectedSelectOptions::from(self.get_selected_options(cell_data.clone()));
let selected_options = self.get_selected_options(cell_data.clone()).select_options;
filter.is_visible(&selected_options, FieldType::SingleSelect)
}
}

View File

@ -86,20 +86,20 @@ impl TypeOptionTransform for RichTextTypeOption {
}
impl TypeOptionCellData for RichTextTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
}
}
impl CellDataDecoder for RichTextTypeOption {
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
_decoded_field_type: &FieldType,
@ -108,11 +108,11 @@ impl CellDataDecoder for RichTextTypeOption {
Ok(StrCellData::from(cell))
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.to_string()
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
Self::CellData::from(cell).to_string()
}
}

View File

@ -14,9 +14,10 @@ use crate::entities::{
URLTypeOptionPB,
};
use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, URLTypeOption,
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
SingleSelectTypeOption, URLTypeOption,
};
use crate::services::filter::FromFilterString;
@ -53,20 +54,19 @@ pub trait TypeOption {
}
pub trait TypeOptionCellData: TypeOption {
/// Convert the decoded cell data into corresponding `Protobuf struct`.
/// Encode the cell data into corresponding `Protobuf struct`.
/// For example:
/// FieldType::URL => URLCellDataPB
/// FieldType::Date=> DateCellDataPB
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType;
/// Decodes the opaque cell string to corresponding data struct.
// For example, the cell data is timestamp if its field type is `FieldType::Date`. This cell
// data can not directly show to user. So it needs to be encode as the date string with custom
// format setting. Encode `1647251762` to `"Mar 14,2022`
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>;
/// Parse the opaque [Cell] to corresponding data struct.
/// The [Cell] is a map that stores list of key/value data. Each [TypeOption::CellData]
/// should implement the From<&Cell> trait to parse the [Cell] to corresponding data struct.
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>;
}
pub trait TypeOptionTransform: TypeOption {

View File

@ -13,10 +13,11 @@ use crate::services::cell::{
CellCache, CellDataChangeset, CellDataDecoder, CellFilterCache, CellProtobufBlob,
FromCellChangeset,
};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TypeOption, TypeOptionCellData,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOption,
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
SingleSelectTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOption,
};
pub const CELL_DATA: &str = "data";
@ -36,6 +37,7 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
field_rev: &Field,
) -> FlowyResult<CellProtobufBlob>;
// TODO(nathan): replace cell_changeset with BoxAny to get rid of the serde process.
fn handle_cell_changeset(
&self,
cell_changeset: String,
@ -141,7 +143,7 @@ where
}
}
let cell_data = self.decode_cell_str(cell, decoded_field_type, field)?;
let cell_data = self.decode_cell(cell, decoded_field_type, field)?;
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
// tracing::trace!(
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
@ -217,7 +219,7 @@ where
.get_cell_data(cell, decoded_field_type, field_rev)?
.unbox_or_default::<<Self as TypeOption>::CellData>();
CellProtobufBlob::from(self.convert_to_protobuf(cell_data))
CellProtobufBlob::from(self.protobuf_encode(cell_data))
}
fn handle_cell_changeset(
@ -265,10 +267,10 @@ where
if self.transformable() {
let cell_data = self.transform_type_option_cell(cell, field_type, field);
if let Some(cell_data) = cell_data {
return self.decode_cell_data_to_str(cell_data);
return self.stringify_cell_data(cell_data);
}
}
self.decode_cell_to_str(cell)
self.stringify_cell(cell)
}
fn get_cell_data(

View File

@ -47,20 +47,20 @@ impl From<URLTypeOption> for TypeOptionData {
impl TypeOptionTransform for URLTypeOption {}
impl TypeOptionCellData for URLTypeOption {
fn convert_to_protobuf(
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
cell_data.into()
}
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(URLCellData::from(cell))
}
}
impl CellDataDecoder for URLTypeOption {
fn decode_cell_str(
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
@ -70,16 +70,16 @@ impl CellDataDecoder for URLTypeOption {
return Ok(Default::default());
}
self.decode_cell(cell)
self.parse_cell(cell)
}
fn decode_cell_data_to_str(&self, cell_data: <Self as TypeOption>::CellData) -> String {
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
cell_data.data
}
fn decode_cell_to_str(&self, cell: &Cell) -> String {
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.decode_cell_data_to_str(cell_data)
self.stringify_cell_data(cell_data)
}
}

View File

@ -1,8 +1,9 @@
use flowy_database2::entities::{CellChangesetPB, FieldType};
use flowy_database2::services::cell::ToCellChangeset;
use flowy_database2::services::field::checklist_type_option::ChecklistCellChangeset;
use flowy_database2::services::field::{
ChecklistTypeOption, DateCellData, MultiSelectTypeOption, SelectOptionCellChangeset,
SingleSelectTypeOption, StrCellData, URLCellData,
DateCellData, MultiSelectTypeOption, SelectOptionCellChangeset, SingleSelectTypeOption,
StrCellData, URLCellData,
};
use crate::database::cell_test::script::CellScript::UpdateCell;
@ -39,13 +40,11 @@ async fn grid_cell_update() {
SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id)
.to_cell_changeset_str()
},
FieldType::Checklist => {
let type_option = field
.get_type_option::<ChecklistTypeOption>(field.field_type)
.unwrap();
SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id)
.to_cell_changeset_str()
},
FieldType::Checklist => ChecklistCellChangeset {
insert_options: vec!["new option".to_string()],
..Default::default()
}
.to_cell_changeset_str(),
FieldType::Checkbox => "1".to_string(),
FieldType::URL => "1".to_string(),
};

View File

@ -5,11 +5,14 @@ use collab_database::fields::Field;
use collab_database::rows::{CreateRowParams, Row, RowId};
use strum::EnumCount;
use flowy_database2::entities::{DatabaseLayoutPB, FieldType, FilterPB, RowPB};
use flowy_database2::entities::{DatabaseLayoutPB, FieldType, FilterPB, RowPB, SelectOptionPB};
use flowy_database2::services::cell::{CellBuilder, ToCellChangeset};
use flowy_database2::services::database::DatabaseEditor;
use flowy_database2::services::field::checklist_type_option::{
ChecklistCellChangeset, ChecklistTypeOption,
};
use flowy_database2::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateCellChangeset, MultiSelectTypeOption, SelectOption,
CheckboxTypeOption, DateCellChangeset, MultiSelectTypeOption, SelectOption,
SelectOptionCellChangeset, SingleSelectTypeOption,
};
use flowy_database2::services::share::csv::{CSVFormat, ImportResult};
@ -205,6 +208,36 @@ impl DatabaseEditorTest {
.await
}
pub(crate) async fn set_checklist_cell(
&mut self,
row_id: RowId,
f: Box<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>>,
) -> FlowyResult<()> {
let field = self
.editor
.get_fields(&self.view_id, None)
.iter()
.find(|field| {
let field_type = FieldType::from(field.field_type);
field_type == FieldType::Checklist
})
.unwrap()
.clone();
let options = self
.editor
.get_checklist_option(row_id.clone(), &field.id)
.await
.options;
let cell_changeset = ChecklistCellChangeset {
selected_option_ids: f(options),
..Default::default()
};
self
.editor
.set_checklist_options(&self.view_id, row_id, &field.id, cell_changeset)
.await
}
pub(crate) async fn update_single_select_cell(
&mut self,
row_id: RowId,
@ -320,7 +353,7 @@ impl<'a> TestRowBuilder<'a> {
{
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
let type_option = single_select_field
.get_type_option::<ChecklistTypeOption>(FieldType::SingleSelect)
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
.unwrap();
let option = f(type_option.options);
self
@ -336,7 +369,7 @@ impl<'a> TestRowBuilder<'a> {
{
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
let type_option = multi_select_field
.get_type_option::<ChecklistTypeOption>(FieldType::MultiSelect)
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
.unwrap();
let options = f(type_option.options);
let ops_ids = options
@ -350,23 +383,11 @@ impl<'a> TestRowBuilder<'a> {
multi_select_field.id.clone()
}
pub fn insert_checklist_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
{
pub fn insert_checklist_cell(&mut self, option_names: Vec<String>) -> String {
let checklist_field = self.field_with_type(&FieldType::Checklist);
let type_option = checklist_field
.get_type_option::<ChecklistTypeOption>(FieldType::Checklist)
.unwrap();
let options = f(type_option.options);
let ops_ids = options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>();
self
.cell_build
.insert_select_option_cell(&checklist_field.id, ops_ids);
.insert_checklist_cell(&checklist_field.id, option_names);
checklist_field.id.clone()
}

View File

@ -8,6 +8,10 @@ async fn grid_filter_checklist_is_incomplete_test() {
let expected = 5;
let row_count = test.rows.len();
let scripts = vec![
UpdateChecklistCell {
row_id: test.rows[0].id.clone(),
f: Box::new(|options| options.into_iter().map(|option| option.id).collect()),
},
CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsIncomplete,
changed: Some(FilterRowChanged {
@ -26,6 +30,10 @@ async fn grid_filter_checklist_is_complete_test() {
let expected = 1;
let row_count = test.rows.len();
let scripts = vec![
UpdateChecklistCell {
row_id: test.rows[0].id.clone(),
f: Box::new(|options| options.into_iter().map(|option| option.id).collect()),
},
CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsComplete,
changed: Some(FilterRowChanged {

View File

@ -10,8 +10,9 @@ use collab_database::rows::{Row, RowId};
use futures::TryFutureExt;
use tokio::sync::broadcast::Receiver;
use flowy_database2::entities::{UpdateFilterParams, UpdateFilterPayloadPB, CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB};
use flowy_database2::entities::{UpdateFilterParams, UpdateFilterPayloadPB, CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB, SelectOptionPB};
use flowy_database2::services::database_view::DatabaseViewChanged;
use flowy_database2::services::field::SelectOption;
use flowy_database2::services::filter::FilterType;
use crate::database::database_editor::DatabaseEditorTest;
@ -27,6 +28,10 @@ pub enum FilterScript {
text: String,
changed: Option<FilterRowChanged>,
},
UpdateChecklistCell{
row_id: RowId,
f: Box<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>> ,
},
UpdateSingleSelectCell {
row_id: RowId,
option_id: String,
@ -133,6 +138,9 @@ impl DatabaseFilterTest {
self.assert_future_changed(changed).await;
self.update_text_cell(row_id, &text).await.unwrap();
}
FilterScript::UpdateChecklistCell { row_id, f } => {
self.set_checklist_cell( row_id, f).await.unwrap();
}
FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => {
self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap());
self.assert_future_changed(changed).await;

View File

@ -3,15 +3,14 @@
// #![allow(unused_imports)]
use crate::database::database_editor::TestRowBuilder;
use crate::database::mock_data::{
COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER,
};
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData};
use collab_database::views::{DatabaseLayout, DatabaseView};
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
use flowy_database2::services::field::{
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat,
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption, TimeFormat,
};
use strum::IntoEnumIterator;
@ -102,11 +101,11 @@ pub fn make_test_board() -> DatabaseData {
fields.push(url);
},
FieldType::Checklist => {
let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple);
let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange);
let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow);
let mut type_option = ChecklistTypeOption::default();
type_option.options.extend(vec![option1, option2, option3]);
// let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple);
// let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange);
// let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow);
let type_option = ChecklistTypeOption::default();
// type_option.options.extend(vec![option1, option2, option3]);
let checklist_field = FieldBuilder::new(field_type.clone(), type_option)
.name("TODO")
.visibility(true)

View File

@ -1,16 +1,14 @@
use crate::database::mock_data::{
COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER,
};
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData};
use collab_database::views::{DatabaseLayout, DatabaseView};
use crate::database::database_editor::TestRowBuilder;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
use flowy_database2::services::field::{
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption,
TimeFormat,
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat,
};
use strum::IntoEnumIterator;
@ -103,11 +101,11 @@ pub fn make_test_grid() -> DatabaseData {
fields.push(url);
},
FieldType::Checklist => {
let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple);
let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange);
let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow);
let mut type_option = ChecklistTypeOption::default();
type_option.options.extend(vec![option1, option2, option3]);
// let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple);
// let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange);
// let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow);
let type_option = ChecklistTypeOption::default();
// type_option.options.extend(vec![option1, option2, option3]);
let checklist_field = FieldBuilder::new(field_type.clone(), type_option)
.name("TODO")
.visibility(true)
@ -135,11 +133,13 @@ pub fn make_test_grid() -> DatabaseData {
),
FieldType::MultiSelect => row_builder
.insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
FieldType::Checklist => row_builder.insert_checklist_cell(|options| options),
FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
FieldType::URL => {
row_builder.insert_url_cell("AppFlowy website - https://www.appflowy.io")
},
FieldType::Checklist => {
row_builder.insert_checklist_cell(vec!["First thing".to_string()])
},
_ => "".to_owned(),
};
}

View File

@ -14,6 +14,6 @@ pub const COMPLETED: &str = "Completed";
pub const PLANNED: &str = "Planned";
pub const PAUSED: &str = "Paused";
pub const FIRST_THING: &str = "Wake up at 6:00 am";
pub const SECOND_THING: &str = "Get some coffee";
pub const THIRD_THING: &str = "Start working";
// pub const FIRST_THING: &str = "Wake up at 6:00 am";
// pub const SECOND_THING: &str = "Get some coffee";
// pub const THIRD_THING: &str = "Start working";

View File

@ -28,13 +28,14 @@ async fn export_csv_test() {
let database = test.editor.clone();
let s = database.export_csv(CSVFormat::Original).await.unwrap();
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Updated At,Created At
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,"Wake up at 6:00 am,Get some coffee,Start working",2022/03/14,2022/03/14
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,2022/03/14,2022/03/14
,$2,2022/03/14,,"Google,Twitter",Yes,,,2022/03/14,2022/03/14
C,$3,2022/03/14,Completed,Facebook,No,,,2022/03/14,2022/03/14
DA,$14,2022/11/17,Completed,,No,,,2022/11/17,2022/11/17
AE,,2022/11/13,Planned,,No,,,2022/11/13,2022/11/13
AE,$5,2022/12/24,Planned,,Yes,,,2022/12/24,2022/12/24
"#;
println!("{}", s);
assert_eq!(s, expected);
}