mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: checklist improve (#2653)
* feat: improve checklist * feat: reimplement checklist * test: fix
This commit is contained in:
parent
dc73df4203
commit
107662dceb
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(¶ms.view_id).await?;
|
||||
let data = database_editor
|
||||
.get_checklist_option(params.row_id, ¶ms.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(¶ms.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(¶ms.view_id, params.row_id, ¶ms.field_id, changeset)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub(crate) async fn update_date_cell_handler(
|
||||
data: AFPluginData<DateChangesetPB>,
|
||||
|
@ -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.
|
||||
|
@ -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),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?;
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mod checklist;
|
||||
mod checklist_entities;
|
||||
|
||||
pub use checklist::*;
|
||||
pub use checklist_entities::*;
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -89,7 +89,7 @@ mod tests {
|
||||
) {
|
||||
assert_eq!(
|
||||
type_option
|
||||
.decode_cell_str(
|
||||
.decode_cell(
|
||||
&NumberCellData(input_str.to_owned()).into(),
|
||||
field_type,
|
||||
field
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>>();
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user