Explorar o código

Merge branch 'main' into feat/flowy-overlay

Nathan.fooo %!s(int64=2) %!d(string=hai) anos
pai
achega
f54f90b647
Modificáronse 26 ficheiros con 766 adicións e 203 borrados
  1. 62 27
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  2. 70 32
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  3. 10 6
      frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart
  4. 0 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  5. 2 2
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  6. 43 9
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  7. 6 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  8. 62 78
      frontend/app_flowy/packages/appflowy_board/README.md
  9. BIN=BIN
      frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg
  10. 1 1
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  11. 3 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  12. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
  13. 2 2
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  14. 1 1
      frontend/rust-lib/flowy-grid/src/services/block_manager.rs
  15. 0 1
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  16. 2 0
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  17. 5 2
      frontend/rust-lib/flowy-grid/src/services/group/action.rs
  18. 8 3
      frontend/rust-lib/flowy-grid/src/services/group/controller.rs
  19. 77 21
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs
  20. 2 0
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs
  21. 9 3
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  22. 7 3
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  23. 1 5
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  24. 387 0
      shared-lib/lib-ot/src/codec/markdown/markdown_encoder.rs
  25. 1 1
      shared-lib/lib-ot/src/codec/markdown/mod.rs
  26. 4 0
      shared-lib/lib-ot/src/rich_text/attributes.rs

+ 62 - 27
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<BoardEvent, BoardState> {
     boardController.clear();
 
     //
-    List<AFBoardColumnData> columns = groups.map((group) {
+    List<AFBoardColumnData> 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<BoardEvent, BoardState> {
     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<BoardEvent, BoardState> {
 
   List<AFColumnItem> _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 <AFColumnItem>[...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";
+}

+ 70 - 32
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<BoardBloc, BoardState>(
-        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<BoardContent> {
       child: BlocBuilder<BoardBloc, BoardState>(
         buildWhen: (previous, current) => previous.groupIds != current.groupIds,
         builder: (context, state) {
-          final theme = context.read<AppTheme>();
+          final column = Column(
+            children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
+          );
+
           return Container(
-            color: theme.surface,
+            color: context.read<AppTheme>().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<BoardBloc>().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<BoardContent> {
     );
   }
 
+  Expanded _buildBoard(BuildContext context) {
+    return Expanded(
+      child: AFBoard(
+        scrollManager: scrollManager,
+        scrollController: scrollController,
+        dataController: context.read<BoardBloc>().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<BoardContent> {
     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<BoardContent> {
           color: context.read<AppTheme>().textColor,
         ),
       ),
+      icon: _buildHeaderIcon(boardCustomData),
       addIcon: SizedBox(
         height: 20,
         width: 20,
@@ -182,7 +183,9 @@ class _BoardContentState extends State<BoardContent> {
   }
 
   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<BoardContent> {
       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;
+}

+ 10 - 6
frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart

@@ -14,33 +14,37 @@ class EditableCellNotifier {
 }
 
 class EditableRowNotifier {
-  Map<EditableCellId, EditableCellNotifier> cells = {};
+  final Map<EditableCellId, EditableCellNotifier> _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();
   }
 }
 

+ 0 - 1
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';
 

+ 2 - 2
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -89,20 +89,20 @@ class _BoardCardState extends State<BoardCard> {
     List<GridCellIdentifier> cells,
   ) {
     final List<Widget> 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),

+ 43 - 9
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<OnReceiveFields, VoidCallback> _fieldCallbackMap = {};
   final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
-
-  _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
-  List<String> _groupFieldIds = [];
   final GridFFIService _gridFFIService;
   final SettingFFIService _settingFFIService;
 
+  _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
+  final Map<String, GridGroupConfigurationPB> _configurationByFieldId = {};
+
   List<GridFieldContext> 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;
 }

+ 6 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart

@@ -31,10 +31,15 @@ class GridGroupList extends StatelessWidget {
       child: BlocBuilder<GridGroupBloc, GridGroupState>(
         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(

+ 62 - 78
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.
+<h1 align="center"><b>AppFlowy Board</b></h1>
 
+<p align="center">A customizable and draggable Kanban Board widget for Flutter</p>
 
-**appflowy_board** will be a standard git repository when it becomes stable.
-## Getting Started
+<p align="center">
+    <a href="https://discord.gg/ZCCYN4Anzq"><b>Discord</b></a> •
+    <a href="https://twitter.com/appflowy"><b>Twitter</b></a>
+</p>
 
-<p>
-<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif?raw=true" width="680" title="AppFlowyBoard">
+<p align="center">
 <img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif?raw=true" width="680" title="AppFlowyBoard">
 </p>
 
+## 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.
+<p align="center">
+<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif?raw=true" width="680" title="AppFlowyBoard">
+</p>
+
+
+## Getting Started
+Add the AppFlowy Board [Flutter package](https://docs.flutter.dev/development/packages-and-plugins/using-packages) to your environment.
+
+With Flutter:
+```dart
+flutter pub add appflowy_board
+```
+
+This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):
 ```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"),
-    ]);
-
-    final column3 = BoardColumnData(id: "Done", items: []);
-
-    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'),
-          ),
-        ),
-      ),
-    );
-  }
-```
+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.
+<p>
+<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg?raw=true" width="100%" title="AppFlowyBoard">
+</p>
+
+## 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.
+
+
+

BIN=BIN
frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg


+ 1 - 1
frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart

@@ -66,7 +66,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
         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'),

+ 3 - 3
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,

+ 1 - 1
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(

+ 2 - 2
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(),
         }
     }
 }

+ 1 - 1
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]);

+ 0 - 1
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<Self>

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

+ 5 - 2
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<CellRevision> {
+        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<GroupChangesetPB>;
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
-
     fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec<GroupChangesetPB>;
 }

+ 8 - 3
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<RowRevision>], 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<GroupedRow> = 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::<P>()?;
                 for group in self.group_ctx.concrete_groups() {
                     if self.can_group(&group.filter_content, &cell_data) {

+ 77 - 21
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<CheckboxGroupConfigurationRevision>
 
 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<CellRevision> {
+        Some(CellRevision::new(UNCHECK.to_string()))
+    }
+
+    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool {
+        if cell_data.is_check() {
+            content == CHECK
+        } else {
+            content == UNCHECK
+        }
     }
 
-    fn add_row_if_match(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
-        todo!()
+    fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
+        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 remove_row_if_match(
-        &mut self,
-        _row_rev: &RowRevision,
-        _cell_data: &Self::CellDataType,
-    ) -> Vec<GroupChangesetPB> {
-        todo!()
+    fn remove_row_if_match(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
+        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, _context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
-        todo!()
+    fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
+        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<Self::TypeOptionType>,
     ) -> Vec<GeneratedGroup> {
         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]
     }

+ 2 - 0
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) {}
 }

+ 9 - 3
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<GroupChangesetPB> {
+    fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         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();

+ 7 - 3
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<GroupChangesetPB> {
+    fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         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();

+ 1 - 5
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<GroupChangesetPB> {
+pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> Option<GroupChangesetPB> {
     let mut changeset = GroupChangesetPB::new(group.id.clone());
     let MoveGroupRowContext {
         row_rev,

+ 387 - 0
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_ <u>underlined</u> \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~~ <mark>highlighted</mark>\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<RichTextAttributes>) -> 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<String> = Vec::new();
+    let mut iterator = DeltaIterator::new(delta);
+    let mut current_block_style: Option<Attribute> = 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<RichTextAttributeKey, RichTextAttributeValue> = HashMap::new();
+
+    for key in current_inline_style
+        .clone()
+        .keys()
+        .collect::<Vec<&RichTextAttributeKey>>()
+        .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("</u>")
+            } else {
+                buffer.push_str("<u>")
+            }
+        }
+        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("</mark>")
+            } else {
+                buffer.push_str("<mark>")
+            }
+        }
+        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<Attribute>,
+    current_block_lines: &mut Vec<String>,
+    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, &current_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<Attribute>,
+    current_block_lines: &mut Vec<String>,
+    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(&current_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(&current_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 {
+        }
+    }
+}

+ 1 - 1
shared-lib/lib-ot/src/codec/markdown/mod.rs

@@ -1 +1 @@
-
+pub mod markdown_encoder;

+ 4 - 0
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<RichTextAttributeKey> = HashSet::from_iter(vec![
         RichTextAttributeKey::Header,