diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 26dd15038a..11f4cf6910 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:collection'; + import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; @@ -12,7 +14,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'dart:collection'; import 'board_data_controller.dart'; import 'group_controller.dart'; @@ -164,12 +165,17 @@ class BoardBloc extends Bloc { boardController.clear(); // - List columns = groups.map((group) { + List columns = groups + .where((group) => fieldController.getField(group.fieldId) != null) + .map((group) { return AFBoardColumnData( id: group.groupId, name: group.desc, items: _buildRows(group), - customData: group, + customData: BoardCustomData( + group: group, + fieldContext: fieldController.getField(group.fieldId)!, + ), ); }).toList(); boardController.addColumns(columns); @@ -177,6 +183,7 @@ class BoardBloc extends Bloc { for (final group in groups) { final delegate = GroupControllerDelegateImpl( controller: boardController, + fieldController: fieldController, onNewColumnItem: (groupId, row, index) { add(BoardEvent.didCreateRow(groupId, row, index)); }, @@ -238,10 +245,8 @@ class BoardBloc extends Bloc { List _buildRows(GroupPB group) { final items = group.rows.map((row) { - return BoardColumnItem( - row: row, - fieldId: group.fieldId, - ); + final fieldContext = fieldController.getField(group.fieldId); + return BoardColumnItem(row: row, fieldContext: fieldContext!); }).toList(); return [...items]; @@ -332,15 +337,11 @@ class GridFieldEquatable extends Equatable { class BoardColumnItem extends AFColumnItem { final RowPB row; - - final String fieldId; - - final bool requestFocus; + final GridFieldContext fieldContext; BoardColumnItem({ required this.row, - required this.fieldId, - this.requestFocus = false, + required this.fieldContext, }); @override @@ -348,24 +349,29 @@ class BoardColumnItem extends AFColumnItem { } class GroupControllerDelegateImpl extends GroupControllerDelegate { + final GridFieldController fieldController; final AFBoardDataController controller; final void Function(String, RowPB, int?) onNewColumnItem; GroupControllerDelegateImpl({ required this.controller, + required this.fieldController, required this.onNewColumnItem, }); @override void insertRow(GroupPB group, RowPB row, int? index) { + final fieldContext = fieldController.getField(group.fieldId); + if (fieldContext == null) { + Log.warn("FieldContext should not be null"); + return; + } + if (index != null) { - final item = BoardColumnItem(row: row, fieldId: group.fieldId); + final item = BoardColumnItem(row: row, fieldContext: fieldContext); controller.insertColumnItem(group.groupId, index, item); } else { - final item = BoardColumnItem( - row: row, - fieldId: group.fieldId, - ); + final item = BoardColumnItem(row: row, fieldContext: fieldContext); controller.addColumnItem(group.groupId, item); } } @@ -377,22 +383,25 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(GroupPB group, RowPB row) { + final fieldContext = fieldController.getField(group.fieldId); + if (fieldContext == null) { + Log.warn("FieldContext should not be null"); + return; + } controller.updateColumnItem( group.groupId, - BoardColumnItem( - row: row, - fieldId: group.fieldId, - ), + BoardColumnItem(row: row, fieldContext: fieldContext), ); } @override void addNewRow(GroupPB group, RowPB row, int? index) { - final item = BoardColumnItem( - row: row, - fieldId: group.fieldId, - requestFocus: true, - ); + final fieldContext = fieldController.getField(group.fieldId); + if (fieldContext == null) { + Log.warn("FieldContext should not be null"); + return; + } + final item = BoardColumnItem(row: row, fieldContext: fieldContext); if (index != null) { controller.insertColumnItem(group.groupId, index, item); @@ -414,3 +423,29 @@ class BoardEditingRow { required this.index, }); } + +class BoardCustomData { + final GroupPB group; + final GridFieldContext fieldContext; + BoardCustomData({ + required this.group, + required this.fieldContext, + }); + + CheckboxGroup? asCheckboxGroup() { + if (fieldType != FieldType.Checkbox) return null; + return CheckboxGroup(group); + } + + FieldType get fieldType => fieldContext.fieldType; +} + +class CheckboxGroup { + final GroupPB group; + + CheckboxGroup(this.group); + +// Hardcode value: "Yes" that equal to the value defined in Rust +// pub const CHECK: &str = "Yes"; + bool get isCheck => group.groupId == "Yes"; +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index f7a10619c0..d8e9550d0e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -18,7 +18,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/group.pbserver.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../grid/application/row/row_cache.dart'; @@ -37,8 +37,7 @@ class BoardPage extends StatelessWidget { create: (context) => BoardBloc(view: view)..add(const BoardEvent.initial()), child: BlocBuilder( - buildWhen: (previous, current) => - previous.loadingState != current.loadingState, + buildWhen: (p, c) => p.loadingState != c.loadingState, builder: (context, state) { return state.loadingState.map( loading: (_) => @@ -85,36 +84,15 @@ class _BoardContentState extends State { child: BlocBuilder( buildWhen: (previous, current) => previous.groupIds != current.groupIds, builder: (context, state) { - final theme = context.read(); + final column = Column( + children: [const _ToolbarBlocAdaptor(), _buildBoard(context)], + ); + return Container( - color: theme.surface, + color: context.read().surface, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - const _ToolbarBlocAdaptor(), - Expanded( - child: AFBoard( - key: UniqueKey(), - scrollManager: scrollManager, - scrollController: scrollController, - dataController: context.read().boardController, - headerBuilder: _buildHeader, - footBuilder: _buildFooter, - cardBuilder: (_, column, columnItem) => _buildCard( - context, - column, - columnItem, - ), - columnConstraints: - const BoxConstraints.tightFor(width: 300), - config: AFBoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ), - ), - ), - ], - ), + child: column, ), ); }, @@ -122,6 +100,27 @@ class _BoardContentState extends State { ); } + Expanded _buildBoard(BuildContext context) { + return Expanded( + child: AFBoard( + scrollManager: scrollManager, + scrollController: scrollController, + dataController: context.read().boardController, + headerBuilder: _buildHeader, + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), + columnConstraints: const BoxConstraints.tightFor(width: 300), + config: AFBoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ); + } + void _handleEditState(BoardState state, BuildContext context) { state.editingRow.fold( () => null, @@ -153,6 +152,7 @@ class _BoardContentState extends State { BuildContext context, AFBoardColumnData columnData, ) { + final boardCustomData = columnData.customData as BoardCustomData; return AppFlowyColumnHeader( title: Flexible( fit: FlexFit.tight, @@ -163,6 +163,7 @@ class _BoardContentState extends State { color: context.read().textColor, ), ), + icon: _buildHeaderIcon(boardCustomData), addIcon: SizedBox( height: 20, width: 20, @@ -182,7 +183,9 @@ class _BoardContentState extends State { } Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { - final group = columnData.customData as GroupPB; + final boardCustomData = columnData.customData as BoardCustomData; + final group = boardCustomData.group; + if (group.isDefault) { return const SizedBox(); } else { @@ -247,7 +250,7 @@ class _BoardContentState extends State { child: BoardCard( gridId: gridId, groupId: column.id, - fieldId: boardColumnItem.fieldId, + fieldId: boardColumnItem.fieldContext.id, isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, @@ -325,3 +328,38 @@ extension HexColor on Color { return Color(int.parse(buffer.toString(), radix: 16)); } } + +Widget? _buildHeaderIcon(BoardCustomData customData) { + Widget? widget; + switch (customData.fieldType) { + case FieldType.Checkbox: + final group = customData.asCheckboxGroup()!; + if (group.isCheck) { + widget = svgWidget('editor/editor_check'); + } else { + widget = svgWidget('editor/editor_uncheck'); + } + break; + case FieldType.DateTime: + break; + case FieldType.MultiSelect: + break; + case FieldType.Number: + break; + case FieldType.RichText: + break; + case FieldType.SingleSelect: + break; + case FieldType.URL: + break; + } + + if (widget != null) { + widget = SizedBox( + width: 20, + height: 20, + child: widget, + ); + } + return widget; +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart index 580ebe6c5e..e8a0406e0f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart @@ -14,33 +14,37 @@ class EditableCellNotifier { } class EditableRowNotifier { - Map cells = {}; + final Map _cells = {}; void insertCell( GridCellIdentifier cellIdentifier, EditableCellNotifier notifier, ) { - cells[EditableCellId.from(cellIdentifier)] = notifier; + _cells[EditableCellId.from(cellIdentifier)] = notifier; } void becomeFirstResponder() { - for (final notifier in cells.values) { + for (final notifier in _cells.values) { notifier.becomeFirstResponder.notify(); } } void resignFirstResponder() { - for (final notifier in cells.values) { + for (final notifier in _cells.values) { notifier.resignFirstResponder.notify(); } } + void clear() { + _cells.clear(); + } + void dispose() { - for (final notifier in cells.values) { + for (final notifier in _cells.values) { notifier.resignFirstResponder.notify(); } - cells.clear(); + _cells.clear(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 02ab521222..99be8cdb3d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.da import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'board_cell.dart'; import 'define.dart'; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 924faef0a5..9d5aefd9bc 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -89,20 +89,20 @@ class _BoardCardState extends State { List cells, ) { final List children = []; + rowNotifier.clear(); cells.asMap().forEach( (int index, GridCellIdentifier cellId) { final cellNotifier = EditableCellNotifier(); Widget child = widget.cellBuilder.buildCell( widget.groupId, cellId, - widget.isEditing, + index == 0 ? widget.isEditing : false, cellNotifier, ); if (index == 0) { rowNotifier.insertCell(cellId, cellNotifier); } - child = Padding( key: cellId.key(), padding: const EdgeInsets.only(left: 4, right: 4), diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart index e9bda8eef5..73127ed6df 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart @@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart'; import 'package:flutter/foundation.dart'; import '../row/row_cache.dart'; @@ -35,12 +36,12 @@ class GridFieldController { final SettingListener _settingListener; final Map _fieldCallbackMap = {}; final Map _changesetCallbackMap = {}; - - _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier(); - List _groupFieldIds = []; final GridFFIService _gridFFIService; final SettingFFIService _settingFFIService; + _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier(); + final Map _configurationByFieldId = {}; + List get fieldContexts => [..._fieldNotifier?.fieldContexts ?? []]; @@ -67,31 +68,43 @@ class GridFieldController { //Listen on setting changes _settingListener.start(onSettingUpdated: (result) { result.fold( - (setting) => _updateFieldsWhenSettingChanged(setting), + (setting) => _updateGroupConfiguration(setting), (r) => Log.error(r), ); }); _settingFFIService.getSetting().then((result) { result.fold( - (setting) => _updateFieldsWhenSettingChanged(setting), + (setting) => _updateGroupConfiguration(setting), (err) => Log.error(err), ); }); } - void _updateFieldsWhenSettingChanged(GridSettingPB setting) { - _groupFieldIds = setting.groupConfigurations.items - .map((item) => item.groupFieldId) + GridFieldContext? getField(String fieldId) { + final fields = _fieldNotifier?.fieldContexts + .where( + (element) => element.id == fieldId, + ) .toList(); + if (fields?.isEmpty ?? true) { + return null; + } + return fields!.first; + } + void _updateGroupConfiguration(GridSettingPB setting) { + _configurationByFieldId.clear(); + for (final configuration in setting.groupConfigurations.items) { + _configurationByFieldId[configuration.fieldId] = configuration; + } _updateFieldContexts(); } void _updateFieldContexts() { if (_fieldNotifier != null) { for (var field in _fieldNotifier!.fieldContexts) { - if (_groupFieldIds.contains(field.id)) { + if (_configurationByFieldId[field.id] != null) { field._isGroupField = true; } else { field._isGroupField = false; @@ -277,5 +290,26 @@ class GridFieldContext { bool get isGroupField => _isGroupField; + bool get canGroup { + switch (_field.fieldType) { + case FieldType.Checkbox: + return true; + case FieldType.DateTime: + return false; + case FieldType.MultiSelect: + return true; + case FieldType.Number: + return false; + case FieldType.RichText: + return false; + case FieldType.SingleSelect: + return true; + case FieldType.URL: + return false; + } + + return false; + } + GridFieldContext({required FieldPB field}) : _field = field; } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart index 0a82eef4bb..b400cda0fe 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart @@ -31,10 +31,15 @@ class GridGroupList extends StatelessWidget { child: BlocBuilder( builder: (context, state) { final cells = state.fieldContexts.map((fieldContext) { - return _GridGroupCell( + Widget cell = _GridGroupCell( fieldContext: fieldContext, key: ValueKey(fieldContext.id), ); + + if (!fieldContext.canGroup) { + cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell)); + } + return cell; }).toList(); return ListView.separated( diff --git a/frontend/app_flowy/packages/appflowy_board/README.md b/frontend/app_flowy/packages/appflowy_board/README.md index 38561d13e3..8da1064699 100644 --- a/frontend/app_flowy/packages/appflowy_board/README.md +++ b/frontend/app_flowy/packages/appflowy_board/README.md @@ -1,87 +1,71 @@ # appflowy_board -The **appflowy_board** is a package that is used in [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy). For the moment, this package is iterated very fast. +

AppFlowy Board

+

A customizable and draggable Kanban Board widget for Flutter

-**appflowy_board** will be a standard git repository when it becomes stable. -## Getting Started +

+ Discord • + Twitter +

-

- +

+## Intro + +appflowy_board is a customizable and draggable Kanban Board widget for Flutter. +You can use it to create a Kanban Board tool like those in Trello. + +Check out [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy) to see how appflowy_board is used to build a BoardView database. +

+ +

+ + +## Getting Started +Add the AppFlowy Board [Flutter package](https://docs.flutter.dev/development/packages-and-plugins/using-packages) to your environment. + +With Flutter: ```dart -@override - void initState() { - final column1 = BoardColumnData(id: "To Do", items: [ - TextItem("Card 1"), - TextItem("Card 2"), - TextItem("Card 3"), - TextItem("Card 4"), - ]); - final column2 = BoardColumnData(id: "In Progress", items: [ - TextItem("Card 5"), - TextItem("Card 6"), - ]); +flutter pub add appflowy_board +``` - final column3 = BoardColumnData(id: "Done", items: []); +This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get): +```dart +dependencies: + appflowy_board: ^0.0.6 +``` + +Import the package in your Dart file: +```dart +import 'package:appflowy_board/appflowy_board.dart'; +``` + +## Usage Example +To quickly grasp how it can be used, look at the /example/lib folder. +First, run main.dart to play with the demo. + +Second, let's delve into multi_board_list_example.dart to understand a few key components: +* A Board widget is created via instantiating an AFBoard() object. +* In the AFBoard() object, you can find: + * AFBoardDataController, which is defined in board_data.dart, is feeded with prepopulated mock data. It also contains callback functions to materialize future user data. + * Three builders: AppFlowyColumnHeader, AppFlowyColumnFooter, AppFlowyColumnItemCard. See below image for what they are used for. +

+ +

+ +## Glossary +Please refer to the API documentation. + +## Contributing +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. + +Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. + +## License +Distributed under the AGPLv3 License. See [LICENSE](https://github.com/AppFlowy-IO/AppFlowy-Docs/blob/main/LICENSE) for more information. - boardDataController.addColumn(column1); - boardDataController.addColumn(column2); - boardDataController.addColumn(column3); - super.initState(); - } - @override - Widget build(BuildContext context) { - final config = BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ); - return Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Board( - dataController: boardDataController, - footBuilder: (context, columnData) { - return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), - height: 50, - margin: config.columnItemPadding, - ); - }, - headerBuilder: (context, columnData) { - return AppFlowyColumnHeader( - icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.id), - addIcon: const Icon(Icons.add, size: 20), - moreIcon: const Icon(Icons.more_horiz, size: 20), - height: 50, - margin: config.columnItemPadding, - ); - }, - cardBuilder: (context, item) { - final textItem = item as TextItem; - return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text(textItem.s), - ), - ), - ); - }, - columnConstraints: const BoxConstraints.tightFor(width: 240), - config: BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ), - ), - ), - ); - } -``` \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg new file mode 100644 index 0000000000..76a9fb67f8 Binary files /dev/null and b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg differ diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 1decf21063..c0fc62f8e2 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -66,7 +66,7 @@ class _MultiBoardListExampleState extends State { padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), child: AFBoard( dataController: boardDataController, - footBuilder: (context, columnData) { + footerBuilder: (context, columnData) { return AppFlowyColumnFooter( icon: const Icon(Icons.add, size: 20), title: const Text('New'), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index a565838da4..96874a1425 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -56,7 +56,7 @@ class AFBoard extends StatelessWidget { final AFBoardColumnHeaderBuilder? headerBuilder; /// - final AFBoardColumnFooterBuilder? footBuilder; + final AFBoardColumnFooterBuilder? footerBuilder; /// final AFBoardDataController dataController; @@ -78,7 +78,7 @@ class AFBoard extends StatelessWidget { required this.dataController, required this.cardBuilder, this.background, - this.footBuilder, + this.footerBuilder, this.headerBuilder, this.scrollController, this.scrollManager, @@ -112,7 +112,7 @@ class AFBoard extends StatelessWidget { delegate: phantomController, columnConstraints: columnConstraints, cardBuilder: cardBuilder, - footBuilder: footBuilder, + footBuilder: footerBuilder, headerBuilder: headerBuilder, phantomController: phantomController, onReorder: dataController.moveColumn, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 79fe534941..ce998b365e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -31,7 +31,7 @@ typedef AFBoardColumnCardBuilder = Widget Function( typedef AFBoardColumnHeaderBuilder = Widget? Function( BuildContext context, - AFBoardColumnData headerData, + AFBoardColumnData columnData, ); typedef AFBoardColumnFooterBuilder = Widget Function( diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index de6f920341..d2c6eb0efb 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -44,14 +44,14 @@ pub struct GridGroupConfigurationPB { pub id: String, #[pb(index = 2)] - pub group_field_id: String, + pub field_id: String, } impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationPB { fn from(rev: &GroupConfigurationRevision) -> Self { GridGroupConfigurationPB { id: rev.id.clone(), - group_field_id: rev.field_id.clone(), + field_id: rev.field_id.clone(), } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index 9619bf52ab..800348fb97 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -107,7 +107,7 @@ impl GridBlockManager { let editor = self.get_editor_from_row_id(&changeset.row_id).await?; let _ = editor.update_row(changeset.clone()).await?; match editor.get_row_rev(&changeset.row_id).await? { - None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id), + None => tracing::error!("Update row failed, can't find the row with id: {}", changeset.row_id), Some(row_rev) => { let row_pb = make_row_from_row_rev(row_rev.clone()); let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]); diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 17c52dd3a2..20dd09a5ef 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -182,7 +182,6 @@ pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) - CellRevision::new(data) } -/// If the cell data is not String type, it should impl this trait. /// Deserialize the String into cell specific data type. pub trait FromCellString { fn from_cell_str(s: &str) -> FlowyResult diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index ab8e8bd41f..30ff689027 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -96,6 +96,8 @@ impl GridViewRevisionEditor { None => Some(0), Some(_) => None, }; + + self.group_controller.write().await.did_create_row(row_pb, group_id); let inserted_row = InsertedRowPB { row: row_pb.clone(), index, diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs index d19be8395e..b339722905 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -1,13 +1,16 @@ use crate::entities::GroupChangesetPB; use crate::services::group::controller::MoveGroupRowContext; -use flowy_grid_data_model::revision::RowRevision; +use flowy_grid_data_model::revision::{CellRevision, RowRevision}; pub trait GroupAction: Send + Sync { type CellDataType; + fn default_cell_rev(&self) -> Option { + None + } + fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 54d001d4a7..75bc16f3b2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -7,7 +7,6 @@ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer, }; - use std::marker::PhantomData; use std::sync::Arc; @@ -16,6 +15,7 @@ use std::sync::Arc; // a new row. pub trait GroupController: GroupControllerSharedOperation + Send + Sync { fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); + fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str); } pub trait GroupGenerator { @@ -193,9 +193,14 @@ where #[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))] fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { for row_rev in row_revs { - if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { + let cell_rev = match row_rev.cells.get(&self.field_id) { + None => self.default_cell_rev(), + Some(cell_rev) => Some(cell_rev.clone()), + }; + + if let Some(cell_rev) = cell_rev { let mut grouped_rows: Vec = vec![]; - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev); let cell_data = cell_bytes.parser::

()?; for group in self.group_ctx.concrete_groups() { if self.can_group(&group.filter_content, &cell_data) { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index 02ff4cb503..c95a6e4af2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupChangesetPB; +use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB}; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GroupContext; @@ -6,8 +6,11 @@ use crate::services::group::controller::{ GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, }; -use crate::services::group::GeneratedGroup; -use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, GroupRevision, RowRevision}; +use crate::services::cell::insert_checkbox_cell; +use crate::services::group::{move_group_row, GeneratedGroup}; +use flowy_grid_data_model::revision::{ + CellRevision, CheckboxGroupConfigurationRevision, FieldRevision, GroupRevision, RowRevision, +}; pub type CheckboxGroupController = GenericGroupController< CheckboxGroupConfigurationRevision, @@ -20,30 +23,83 @@ pub type CheckboxGroupContext = GroupContext impl GroupAction for CheckboxGroupController { type CellDataType = CheckboxCellData; - fn can_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool { - false + fn default_cell_rev(&self) -> Option { + Some(CellRevision::new(UNCHECK.to_string())) } - fn add_row_if_match(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec { - todo!() + fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool { + if cell_data.is_check() { + content == CHECK + } else { + content == UNCHECK + } } - fn remove_row_if_match( - &mut self, - _row_rev: &RowRevision, - _cell_data: &Self::CellDataType, - ) -> Vec { - todo!() + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_groups(|group| { + let mut changeset = GroupChangesetPB::new(group.id.clone()); + let is_contained = group.contains_row(&row_rev.id); + if group.id == CHECK && cell_data.is_check() { + if !is_contained { + let row_pb = RowPB::from(row_rev); + changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone())); + group.add_row(row_pb); + } + } else if is_contained { + changeset.deleted_rows.push(row_rev.id.clone()); + group.remove_row(&row_rev.id); + } + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets } - fn move_row(&mut self, _cell_data: &Self::CellDataType, _context: MoveGroupRowContext) -> Vec { - todo!() + fn remove_row_if_match(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_groups(|group| { + let mut changeset = GroupChangesetPB::new(group.id.clone()); + if group.contains_row(&row_rev.id) { + changeset.deleted_rows.push(row_rev.id.clone()); + group.remove_row(&row_rev.id); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { + let mut group_changeset = vec![]; + self.group_ctx.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset } } impl GroupController for CheckboxGroupController { - fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) { - todo!() + fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + match self.group_ctx.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_, group)) => { + let is_check = group.id == CHECK; + let cell_rev = insert_checkbox_cell(is_check, field_rev); + row_rev.cells.insert(field_rev.id.clone(), cell_rev); + } + } + } + + fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row_pb.clone()) + } } } @@ -58,13 +114,13 @@ impl GroupGenerator for CheckboxGroupGenerator { _type_option: &Option, ) -> Vec { let check_group = GeneratedGroup { - group_rev: GroupRevision::new("true".to_string(), CHECK.to_string()), - filter_content: "".to_string(), + group_rev: GroupRevision::new(CHECK.to_string(), "".to_string()), + filter_content: CHECK.to_string(), }; let uncheck_group = GeneratedGroup { - group_rev: GroupRevision::new("false".to_string(), UNCHECK.to_string()), - filter_content: "".to_string(), + group_rev: GroupRevision::new(UNCHECK.to_string(), "".to_string()), + filter_content: UNCHECK.to_string(), }; vec![check_group, uncheck_group] } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs index 938bdd127e..2489df8af2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs @@ -77,4 +77,6 @@ impl GroupControllerSharedOperation for DefaultGroupController { impl GroupController for DefaultGroupController { fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {} + + fn did_create_row(&mut self, _row_rev: &RowPB, _group_id: &str) {} } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 026843bbcc..8d18fa2a83 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupChangesetPB; +use crate::entities::{GroupChangesetPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; use crate::services::group::action::GroupAction; @@ -46,10 +46,10 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { + fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_groups(|group| { - if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); @@ -67,6 +67,12 @@ impl GroupController for MultiSelectGroupController { } } } + + fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row_pb.clone()) + } + } } pub struct MultiSelectGroupGenerator(); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 96b686efe3..25da9eec17 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -46,10 +46,10 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { + fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_groups(|group| { - if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); @@ -65,10 +65,14 @@ impl GroupController for SingleSelectGroupController { Some(group) => { let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); row_rev.cells.insert(field_rev.id.clone(), cell_rev); - group.add_row(RowPB::from(row_rev)); } } } + fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row_pb.clone()) + } + } } pub struct SingleSelectGroupGenerator(); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index 494cd41197..a1d81b0d5c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -62,11 +62,7 @@ pub fn remove_select_option_row( } } -pub fn move_select_option_row( - group: &mut Group, - _cell_data: &SelectOptionCellDataPB, - context: &mut MoveGroupRowContext, -) -> Option { +pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> Option { let mut changeset = GroupChangesetPB::new(group.id.clone()); let MoveGroupRowContext { row_rev, diff --git a/shared-lib/lib-ot/src/codec/markdown/markdown_encoder.rs b/shared-lib/lib-ot/src/codec/markdown/markdown_encoder.rs new file mode 100644 index 0000000000..f338a029a4 --- /dev/null +++ b/shared-lib/lib-ot/src/codec/markdown/markdown_encoder.rs @@ -0,0 +1,387 @@ +use crate::core::{Delta, DeltaIterator}; +use crate::rich_text::{is_block, RichTextAttributeKey, RichTextAttributeValue, RichTextAttributes}; +use std::collections::HashMap; + +const LINEFEEDASCIICODE: i32 = 0x0A; + +#[cfg(test)] +mod tests { + use crate::codec::markdown::markdown_encoder::markdown_encoder; + use crate::rich_text::RichTextDelta; + + #[test] + fn markdown_encoder_header_1_test() { + let json = r#"[{"insert":"header 1"},{"insert":"\n","attributes":{"header":1}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "# header 1\n"); + } + + #[test] + fn markdown_encoder_header_2_test() { + let json = r#"[{"insert":"header 2"},{"insert":"\n","attributes":{"header":2}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "## header 2\n"); + } + + #[test] + fn markdown_encoder_header_3_test() { + let json = r#"[{"insert":"header 3"},{"insert":"\n","attributes":{"header":3}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "### header 3\n"); + } + + #[test] + fn markdown_encoder_bold_italics_underlined_test() { + let json = r#"[{"insert":"bold","attributes":{"bold":true}},{"insert":" "},{"insert":"italics","attributes":{"italic":true}},{"insert":" "},{"insert":"underlined","attributes":{"underline":true}},{"insert":" "},{"insert":"\n","attributes":{"header":3}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "### **bold** _italics_ underlined \n"); + } + #[test] + fn markdown_encoder_strikethrough_highlight_test() { + let json = r##"[{"insert":"strikethrough","attributes":{"strike":true}},{"insert":" "},{"insert":"highlighted","attributes":{"background":"#ffefe3"}},{"insert":"\n"}]"##; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "~~strikethrough~~ highlighted\n"); + } + + #[test] + fn markdown_encoder_numbered_list_test() { + let json = r#"[{"insert":"numbered list\nitem 1"},{"insert":"\n","attributes":{"list":"ordered"}},{"insert":"item 2"},{"insert":"\n","attributes":{"list":"ordered"}},{"insert":"item3"},{"insert":"\n","attributes":{"list":"ordered"}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "numbered list\n\n1. item 1\n1. item 2\n1. item3\n"); + } + + #[test] + fn markdown_encoder_bullet_list_test() { + let json = r#"[{"insert":"bullet list\nitem1"},{"insert":"\n","attributes":{"list":"bullet"}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "bullet list\n\n* item1\n"); + } + + #[test] + fn markdown_encoder_check_list_test() { + let json = r#"[{"insert":"check list\nchecked"},{"insert":"\n","attributes":{"list":"checked"}},{"insert":"unchecked"},{"insert":"\n","attributes":{"list":"unchecked"}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "check list\n\n- [x] checked\n\n- [ ] unchecked\n"); + } + + #[test] + fn markdown_encoder_code_test() { + let json = r#"[{"insert":"code this "},{"insert":"print(\"hello world\")","attributes":{"code":true}},{"insert":"\n"}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "code this `print(\"hello world\")`\n"); + } + + #[test] + fn markdown_encoder_quote_block_test() { + let json = r#"[{"insert":"this is a quote block"},{"insert":"\n","attributes":{"blockquote":true}}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "> this is a quote block\n"); + } + + #[test] + fn markdown_encoder_link_test() { + let json = r#"[{"insert":"appflowy","attributes":{"link":"https://www.appflowy.io/"}},{"insert":"\n"}]"#; + let delta = RichTextDelta::from_json(json).unwrap(); + let md = markdown_encoder(&delta); + assert_eq!(md, "[appflowy](https://www.appflowy.io/)\n"); + } +} + +struct Attribute { + key: RichTextAttributeKey, + value: RichTextAttributeValue, +} + +pub fn markdown_encoder(delta: &Delta) -> String { + let mut markdown_buffer = String::new(); + let mut line_buffer = String::new(); + let mut current_inline_style = RichTextAttributes::default(); + let mut current_block_lines: Vec = Vec::new(); + let mut iterator = DeltaIterator::new(delta); + let mut current_block_style: Option = None; + + while iterator.has_next() { + let operation = iterator.next().unwrap(); + let operation_data = operation.get_data(); + if !operation_data.contains("\n") { + handle_inline( + &mut current_inline_style, + &mut line_buffer, + String::from(operation_data), + operation.get_attributes(), + ) + } else { + handle_line( + &mut line_buffer, + &mut markdown_buffer, + String::from(operation_data), + operation.get_attributes(), + &mut current_block_style, + &mut current_block_lines, + &mut current_inline_style, + ) + } + } + handle_block(&mut current_block_style, &mut current_block_lines, &mut markdown_buffer); + + markdown_buffer +} + +fn handle_inline( + current_inline_style: &mut RichTextAttributes, + buffer: &mut String, + mut text: String, + attributes: RichTextAttributes, +) { + let mut marked_for_removal: HashMap = HashMap::new(); + + for key in current_inline_style + .clone() + .keys() + .collect::>() + .into_iter() + .rev() + { + if is_block(key) { + continue; + } + + if attributes.contains_key(key) { + continue; + } + + let padding = trim_right(buffer); + write_attribute(buffer, key, current_inline_style.get(key).unwrap(), true); + if !padding.is_empty() { + buffer.push_str(&padding) + } + marked_for_removal.insert(key.clone(), current_inline_style.get(key).unwrap().clone()); + } + + for (marked_for_removal_key, marked_for_removal_value) in &marked_for_removal { + current_inline_style.retain(|inline_style_key, inline_style_value| { + inline_style_key != marked_for_removal_key && inline_style_value != marked_for_removal_value + }) + } + + for (key, value) in attributes.iter() { + if is_block(key) { + continue; + } + if current_inline_style.contains_key(key) { + continue; + } + let original_text = text.clone(); + text = text.trim_start().to_string(); + let padding = " ".repeat(original_text.len() - text.len()); + if !padding.is_empty() { + buffer.push_str(&padding) + } + write_attribute(buffer, key, value, false) + } + + buffer.push_str(&text); + *current_inline_style = attributes; +} + +fn trim_right(buffer: &mut String) -> String { + let text = buffer.clone(); + if !text.ends_with(" ") { + return String::from(""); + } + let result = text.trim_end(); + buffer.clear(); + buffer.push_str(result); + " ".repeat(text.len() - result.len()) +} + +fn write_attribute(buffer: &mut String, key: &RichTextAttributeKey, value: &RichTextAttributeValue, close: bool) { + match key { + RichTextAttributeKey::Bold => buffer.push_str("**"), + RichTextAttributeKey::Italic => buffer.push_str("_"), + RichTextAttributeKey::Underline => { + if close { + buffer.push_str("") + } else { + buffer.push_str("") + } + } + RichTextAttributeKey::StrikeThrough => { + if close { + buffer.push_str("~~") + } else { + buffer.push_str("~~") + } + } + RichTextAttributeKey::Link => { + if close { + buffer.push_str(format!("]({})", value.0.as_ref().unwrap()).as_str()) + } else { + buffer.push_str("[") + } + } + RichTextAttributeKey::Background => { + if close { + buffer.push_str("") + } else { + buffer.push_str("") + } + } + RichTextAttributeKey::CodeBlock => { + if close { + buffer.push_str("\n```") + } else { + buffer.push_str("```\n") + } + } + RichTextAttributeKey::InlineCode => { + if close { + buffer.push_str("`") + } else { + buffer.push_str("`") + } + } + _ => {} + } +} + +fn handle_line( + buffer: &mut String, + markdown_buffer: &mut String, + data: String, + attributes: RichTextAttributes, + current_block_style: &mut Option, + current_block_lines: &mut Vec, + current_inline_style: &mut RichTextAttributes, +) { + let mut span = String::new(); + for c in data.chars() { + if (c as i32) == LINEFEEDASCIICODE { + if !span.is_empty() { + handle_inline(current_inline_style, buffer, span.clone(), attributes.clone()); + } + handle_inline( + current_inline_style, + buffer, + String::from(""), + RichTextAttributes::default(), + ); + + let line_block_key = attributes.keys().find(|key| { + if is_block(*key) { + return true; + } else { + return false; + } + }); + + match (line_block_key, ¤t_block_style) { + (Some(line_block_key), Some(current_block_style)) + if *line_block_key == current_block_style.key + && *attributes.get(line_block_key).unwrap() == current_block_style.value => + { + current_block_lines.push(buffer.clone()); + } + (None, None) => { + current_block_lines.push(buffer.clone()); + } + _ => { + handle_block(current_block_style, current_block_lines, markdown_buffer); + current_block_lines.clear(); + current_block_lines.push(buffer.clone()); + + match line_block_key { + None => *current_block_style = None, + Some(line_block_key) => { + *current_block_style = Some(Attribute { + key: line_block_key.clone(), + value: attributes.get(line_block_key).unwrap().clone(), + }) + } + } + } + } + buffer.clear(); + span.clear(); + } else { + span.push(c); + } + } + if !span.is_empty() { + handle_inline(current_inline_style, buffer, span.clone(), attributes) + } +} + +fn handle_block( + block_style: &mut Option, + current_block_lines: &mut Vec, + markdown_buffer: &mut String, +) { + if current_block_lines.is_empty() { + return; + } + if !markdown_buffer.is_empty() { + markdown_buffer.push('\n') + } + + match block_style { + None => { + markdown_buffer.push_str(¤t_block_lines.join("\n")); + markdown_buffer.push('\n'); + } + Some(block_style) if block_style.key == RichTextAttributeKey::CodeBlock => { + write_attribute(markdown_buffer, &block_style.key, &block_style.value, false); + markdown_buffer.push_str(¤t_block_lines.join("\n")); + write_attribute(markdown_buffer, &block_style.key, &block_style.value, true); + markdown_buffer.push('\n'); + } + Some(block_style) => { + for line in current_block_lines { + write_block_tag(markdown_buffer, &block_style, false); + markdown_buffer.push_str(line); + markdown_buffer.push('\n'); + } + } + } +} + +fn write_block_tag(buffer: &mut String, block: &Attribute, close: bool) { + if close { + return; + } + + if block.key == RichTextAttributeKey::BlockQuote { + buffer.push_str("> "); + } else if block.key == RichTextAttributeKey::List { + if block.value.0.as_ref().unwrap().eq("bullet") { + buffer.push_str("* "); + } else if block.value.0.as_ref().unwrap().eq("checked") { + buffer.push_str("- [x] "); + } else if block.value.0.as_ref().unwrap().eq("unchecked") { + buffer.push_str("- [ ] "); + } else if block.value.0.as_ref().unwrap().eq("ordered") { + buffer.push_str("1. "); + } else { + buffer.push_str("* "); + } + } else if block.key == RichTextAttributeKey::Header { + if block.value.0.as_ref().unwrap().eq("1") { + buffer.push_str("# "); + } else if block.value.0.as_ref().unwrap().eq("2") { + buffer.push_str("## "); + } else if block.value.0.as_ref().unwrap().eq("3") { + buffer.push_str("### "); + } else if block.key == RichTextAttributeKey::List { + } + } +} diff --git a/shared-lib/lib-ot/src/codec/markdown/mod.rs b/shared-lib/lib-ot/src/codec/markdown/mod.rs index 8b13789179..5a276efda3 100644 --- a/shared-lib/lib-ot/src/codec/markdown/mod.rs +++ b/shared-lib/lib-ot/src/codec/markdown/mod.rs @@ -1 +1 @@ - +pub mod markdown_encoder; diff --git a/shared-lib/lib-ot/src/rich_text/attributes.rs b/shared-lib/lib-ot/src/rich_text/attributes.rs index 54826c49a0..be3b1bbf59 100644 --- a/shared-lib/lib-ot/src/rich_text/attributes.rs +++ b/shared-lib/lib-ot/src/rich_text/attributes.rs @@ -361,6 +361,10 @@ pub fn is_block_except_header(k: &RichTextAttributeKey) -> bool { BLOCK_KEYS.contains(k) } +pub fn is_block(k: &RichTextAttributeKey) -> bool { + BLOCK_KEYS.contains(k) +} + lazy_static! { static ref BLOCK_KEYS: HashSet = HashSet::from_iter(vec![ RichTextAttributeKey::Header,