Browse Source

Merge pull request #1241 from AppFlowy-IO/fix/1193

Fix/1193
Nathan.fooo 2 years ago
parent
commit
591d8a3872
24 changed files with 399 additions and 202 deletions
  1. 2 0
      frontend/app_flowy/ios/Runner/Info.plist
  2. 81 28
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  3. 4 4
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  4. 31 24
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  5. 4 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart
  6. 8 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  7. 10 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  8. 1 1
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  9. 4 0
      frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
  10. 26 30
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  11. 17 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  12. 2 2
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart
  13. 29 4
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart
  14. 6 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart
  15. 11 7
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  16. 68 55
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  17. 11 8
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
  18. 1 1
      frontend/app_flowy/pubspec.lock
  19. 1 1
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  20. 13 13
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  21. 12 12
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs
  22. 9 1
      frontend/rust-lib/flowy-grid/src/services/group/configuration.rs
  23. 4 3
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  24. 44 0
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

+ 2 - 0
frontend/app_flowy/ios/Runner/Info.plist

@@ -45,5 +45,7 @@
 	<array>
 		<string>en</string>
 	</array>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
 </dict>
 </plist>

+ 81 - 28
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -89,18 +89,30 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
               (err) => Log.error(err),
             );
           },
-          didCreateRow: (String groupId, RowPB row, int? index) {
+          didCreateRow: (group, row, int? index) {
             emit(state.copyWith(
               editingRow: Some(BoardEditingRow(
-                columnId: groupId,
+                group: group,
                 row: row,
                 index: index,
               )),
             ));
+            _groupItemStartEditing(group, row, true);
           },
-          endEditRow: (rowId) {
+          startEditingRow: (group, row) {
+            emit(state.copyWith(
+              editingRow: Some(BoardEditingRow(
+                group: group,
+                row: row,
+                index: null,
+              )),
+            ));
+            _groupItemStartEditing(group, row, true);
+          },
+          endEditingRow: (rowId) {
             state.editingRow.fold(() => null, (editingRow) {
               assert(editingRow.row.id == rowId);
+              _groupItemStartEditing(editingRow.group, editingRow.row, false);
               emit(state.copyWith(editingRow: none()));
             });
           },
@@ -122,6 +134,24 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
+  void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
+    final fieldContext = fieldController.getField(group.fieldId);
+    if (fieldContext == null) {
+      Log.warn("FieldContext should not be null");
+      return;
+    }
+
+    boardController.enableGroupDragging(!isEdit);
+    // boardController.updateGroupItem(
+    //   group.groupId,
+    //   GroupItem(
+    //     row: row,
+    //     fieldContext: fieldContext,
+    //     isDraggable: !isEdit,
+    //   ),
+    // );
+  }
+
   void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
     if (fromRow != null) {
       _rowService
@@ -136,11 +166,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     }
   }
 
-  void _moveGroup(String fromColumnId, String toColumnId) {
+  void _moveGroup(String fromGroupId, String toGroupId) {
     _rowService
         .moveGroup(
-      fromGroupId: fromColumnId,
-      toGroupId: toColumnId,
+      fromGroupId: fromGroupId,
+      toGroupId: toGroupId,
     )
         .then((result) {
       result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
@@ -156,7 +186,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     return super.close();
   }
 
-  void initializeGroups(List<GroupPB> groups) {
+  void initializeGroups(List<GroupPB> groupsData) {
     for (var controller in groupControllers.values) {
       controller.dispose();
     }
@@ -164,27 +194,27 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     boardController.clear();
 
     //
-    List<AppFlowyGroupData> columns = groups
+    List<AppFlowyGroupData> groups = groupsData
         .where((group) => fieldController.getField(group.fieldId) != null)
         .map((group) {
       return AppFlowyGroupData(
         id: group.groupId,
         name: group.desc,
-        items: _buildRows(group),
-        customData: BoardCustomData(
+        items: _buildGroupItems(group),
+        customData: GroupData(
           group: group,
           fieldContext: fieldController.getField(group.fieldId)!,
         ),
       );
     }).toList();
-    boardController.addGroups(columns);
+    boardController.addGroups(groups);
 
-    for (final group in groups) {
+    for (final group in groupsData) {
       final delegate = GroupControllerDelegateImpl(
         controller: boardController,
         fieldController: fieldController,
         onNewColumnItem: (groupId, row, index) {
-          add(BoardEvent.didCreateRow(groupId, row, index));
+          add(BoardEvent.didCreateRow(group, row, index));
         },
       );
       final controller = GroupController(
@@ -242,10 +272,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  List<AppFlowyGroupItem> _buildRows(GroupPB group) {
+  List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
     final items = group.rows.map((row) {
       final fieldContext = fieldController.getField(group.fieldId);
-      return BoardColumnItem(row: row, fieldContext: fieldContext!);
+      return GroupItem(
+        row: row,
+        fieldContext: fieldContext!,
+      );
     }).toList();
 
     return <AppFlowyGroupItem>[...items];
@@ -270,11 +303,15 @@ class BoardEvent with _$BoardEvent {
   const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
   const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
   const factory BoardEvent.didCreateRow(
-    String groupId,
+    GroupPB group,
     RowPB row,
     int? index,
   ) = _DidCreateRow;
-  const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
+  const factory BoardEvent.startEditingRow(
+    GroupPB group,
+    RowPB row,
+  ) = _StartEditRow;
+  const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow;
   const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
   const factory BoardEvent.didReceiveGridUpdate(
     GridPB grid,
@@ -334,14 +371,17 @@ class GridFieldEquatable extends Equatable {
   UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
 }
 
-class BoardColumnItem extends AppFlowyGroupItem {
+class GroupItem extends AppFlowyGroupItem {
   final RowPB row;
   final GridFieldContext fieldContext;
 
-  BoardColumnItem({
+  GroupItem({
     required this.row,
     required this.fieldContext,
-  });
+    bool draggable = true,
+  }) {
+    super.draggable = draggable;
+  }
 
   @override
   String get id => row.id;
@@ -367,10 +407,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
     }
 
     if (index != null) {
-      final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+      final item = GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      );
       controller.insertGroupItem(group.groupId, index, item);
     } else {
-      final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+      final item = GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      );
       controller.addGroupItem(group.groupId, item);
     }
   }
@@ -389,7 +435,10 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
     }
     controller.updateGroupItem(
       group.groupId,
-      BoardColumnItem(row: row, fieldContext: fieldContext),
+      GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      ),
     );
   }
 
@@ -400,7 +449,11 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
       Log.warn("FieldContext should not be null");
       return;
     }
-    final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+    final item = GroupItem(
+      row: row,
+      fieldContext: fieldContext,
+      draggable: false,
+    );
 
     if (index != null) {
       controller.insertGroupItem(group.groupId, index, item);
@@ -412,21 +465,21 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 }
 
 class BoardEditingRow {
-  String columnId;
+  GroupPB group;
   RowPB row;
   int? index;
 
   BoardEditingRow({
-    required this.columnId,
+    required this.group,
     required this.row,
     required this.index,
   });
 }
 
-class BoardCustomData {
+class GroupData {
   final GroupPB group;
   final GridFieldContext fieldContext;
-  BoardCustomData({
+  GroupData({
     required this.group,
     required this.fieldContext,
   });

+ 4 - 4
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -87,13 +87,13 @@ class BoardDataController {
               onUpdatedGroup.call(changeset.updateGroups);
             }
 
-            if (changeset.insertedGroups.isNotEmpty) {
-              onInsertedGroup.call(changeset.insertedGroups);
-            }
-
             if (changeset.deletedGroups.isNotEmpty) {
               onDeletedGroup.call(changeset.deletedGroups);
             }
+
+            if (changeset.insertedGroups.isNotEmpty) {
+              onInsertedGroup.call(changeset.insertedGroups);
+            }
           },
           (e) => _onError?.call(e),
         );

+ 31 - 24
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -83,7 +83,7 @@ class _BoardContentState extends State<BoardContent> {
   @override
   Widget build(BuildContext context) {
     return BlocListener<BoardBloc, BoardState>(
-      listener: (context, state) => _handleEditState(state, context),
+      listener: (context, state) => _handleEditStateChanged(state, context),
       child: BlocBuilder<BoardBloc, BoardState>(
         buildWhen: (previous, current) => previous.groupIds != current.groupIds,
         builder: (context, state) {
@@ -128,21 +128,14 @@ class _BoardContentState extends State<BoardContent> {
     );
   }
 
-  void _handleEditState(BoardState state, BuildContext context) {
+  void _handleEditStateChanged(BoardState state, BuildContext context) {
     state.editingRow.fold(
       () => null,
       (editingRow) {
         WidgetsBinding.instance.addPostFrameCallback((_) {
           if (editingRow.index != null) {
-            context
-                .read<BoardBloc>()
-                .add(BoardEvent.endEditRow(editingRow.row.id));
           } else {
-            scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
-              context
-                  .read<BoardBloc>()
-                  .add(BoardEvent.endEditRow(editingRow.row.id));
-            });
+            scrollManager.scrollToBottom(editingRow.group.groupId);
           }
         });
       },
@@ -156,14 +149,14 @@ class _BoardContentState extends State<BoardContent> {
 
   Widget _buildHeader(
     BuildContext context,
-    AppFlowyGroupData columnData,
+    AppFlowyGroupData groupData,
   ) {
-    final boardCustomData = columnData.customData as BoardCustomData;
+    final boardCustomData = groupData.customData as GroupData;
     return AppFlowyGroupHeader(
       title: Flexible(
         fit: FlexFit.tight,
         child: FlowyText.medium(
-          columnData.headerData.groupName,
+          groupData.headerData.groupName,
           fontSize: 14,
           overflow: TextOverflow.clip,
           color: context.read<AppTheme>().textColor,
@@ -180,7 +173,7 @@ class _BoardContentState extends State<BoardContent> {
       ),
       onAddButtonClick: () {
         context.read<BoardBloc>().add(
-              BoardEvent.createHeaderRow(columnData.id),
+              BoardEvent.createHeaderRow(groupData.id),
             );
       },
       height: 50,
@@ -218,15 +211,16 @@ class _BoardContentState extends State<BoardContent> {
 
   Widget _buildCard(
     BuildContext context,
-    AppFlowyGroupData group,
-    AppFlowyGroupItem columnItem,
+    AppFlowyGroupData afGroupData,
+    AppFlowyGroupItem afGroupItem,
   ) {
-    final boardColumnItem = columnItem as BoardColumnItem;
-    final rowPB = boardColumnItem.row;
+    final groupItem = afGroupItem as GroupItem;
+    final groupData = afGroupData.customData as GroupData;
+    final rowPB = groupItem.row;
     final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
 
     /// Return placeholder widget if the rowCache is null.
-    if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
+    if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
 
     final fieldController = context.read<BoardBloc>().fieldController;
     final gridId = context.read<BoardBloc>().gridId;
@@ -241,19 +235,19 @@ class _BoardContentState extends State<BoardContent> {
     context.read<BoardBloc>().state.editingRow.fold(
       () => null,
       (editingRow) {
-        isEditing = editingRow.row.id == columnItem.row.id;
+        isEditing = editingRow.row.id == groupItem.row.id;
       },
     );
 
-    final groupItemId = columnItem.id + group.id;
+    final groupItemId = groupItem.row.id + groupData.group.groupId;
     return AppFlowyGroupCard(
       key: ValueKey(groupItemId),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
       child: BoardCard(
         gridId: gridId,
-        groupId: group.id,
-        fieldId: boardColumnItem.fieldContext.id,
+        groupId: groupData.group.groupId,
+        fieldId: groupItem.fieldContext.id,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,
@@ -264,6 +258,19 @@ class _BoardContentState extends State<BoardContent> {
           rowCache,
           context,
         ),
+        onStartEditing: () {
+          context.read<BoardBloc>().add(
+                BoardEvent.startEditingRow(
+                  groupData.group,
+                  groupItem.row,
+                ),
+              );
+        },
+        onEndEditing: () {
+          context
+              .read<BoardBloc>()
+              .add(BoardEvent.endEditingRow(groupItem.row.id));
+        },
       ),
     );
   }
@@ -345,7 +352,7 @@ extension HexColor on Color {
   }
 }
 
-Widget? _buildHeaderIcon(BoardCustomData customData) {
+Widget? _buildHeaderIcon(GroupData customData) {
   Widget? widget;
   switch (customData.fieldType) {
     case FieldType.Checkbox:

+ 4 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart

@@ -76,6 +76,10 @@ class EditableRowNotifier {
 }
 
 abstract class EditableCell {
+  // Each cell notifier will be bind to the [EditableRowNotifier], which enable
+  // the row notifier receive its cells event. For example: begin editing the
+  // cell or end editing the cell.
+  //
   EditableCellNotifier? get editableNotifier;
 }
 

+ 8 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart

@@ -42,6 +42,9 @@ class _BoardTextCellState extends State<BoardTextCell> {
       focusNode.requestFocus();
     }
 
+    // If the focusNode lost its focus, the widget's editableNotifier will
+    // set to false, which will cause the [EditableRowNotifier] to receive
+    // end edit event.
     focusNode.addListener(() {
       if (!focusNode.hasFocus) {
         focusWhenInit = false;
@@ -131,7 +134,11 @@ class _BoardTextCellState extends State<BoardTextCell> {
       padding: EdgeInsets.symmetric(
         vertical: BoardSizes.cardCellVPadding,
       ),
-      child: FlowyText.medium(state.content, fontSize: 14),
+      child: FlowyText.medium(
+        state.content,
+        fontSize: 14,
+        maxLines: null, // Enable multiple lines
+      ),
     );
   }
 

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

@@ -21,6 +21,8 @@ class BoardCard extends StatefulWidget {
   final CardDataController dataController;
   final BoardCellBuilder cellBuilder;
   final void Function(BuildContext) openCard;
+  final VoidCallback onStartEditing;
+  final VoidCallback onEndEditing;
 
   const BoardCard({
     required this.gridId,
@@ -30,6 +32,8 @@ class BoardCard extends StatefulWidget {
     required this.dataController,
     required this.cellBuilder,
     required this.openCard,
+    required this.onStartEditing,
+    required this.onEndEditing,
     Key? key,
   }) : super(key: key);
 
@@ -56,6 +60,12 @@ class _BoardCardState extends State<BoardCard> {
     rowNotifier.isEditing.addListener(() {
       if (!mounted) return;
       _cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
+
+      if (rowNotifier.isEditing.value) {
+        widget.onStartEditing();
+      } else {
+        widget.onEndEditing();
+      }
     });
 
     popoverController = PopoverController();

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

@@ -78,7 +78,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
             height: 50,
             margin: config.groupItemPadding,
             onAddButtonClick: () {
-              boardController.scrollToBottom(columnData.id, (p0) {});
+              boardController.scrollToBottom(columnData.id);
             },
           );
         },

+ 4 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -32,4 +32,8 @@ class Log {
           'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
     }
   }
+
+  static void error(String? message) {
+    debugPrint('AppFlowyBoard: ❌[Error] - ${DateTime.now().second}=> $message');
+  }
 }

+ 26 - 30
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -11,10 +11,11 @@ import 'reorder_phantom/phantom_controller.dart';
 import '../rendering/board_overlay.dart';
 
 class AppFlowyBoardScrollController {
-  AppFlowyBoardState? _groupState;
+  AppFlowyBoardState? _boardState;
 
-  void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
-    _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
+  void scrollToBottom(String groupId,
+      {void Function(BuildContext)? completed}) {
+    _boardState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
   }
 }
 
@@ -39,9 +40,6 @@ class AppFlowyBoardConfig {
 }
 
 class AppFlowyBoard extends StatelessWidget {
-  /// The direction to use as the main axis.
-  final Axis direction = Axis.vertical;
-
   /// The widget that will be rendered as the background of the board.
   final Widget? background;
 
@@ -94,11 +92,7 @@ class AppFlowyBoard extends StatelessWidget {
   ///
   final AppFlowyBoardScrollController? boardScrollController;
 
-  final AppFlowyBoardState _groupState = AppFlowyBoardState();
-
-  late final BoardPhantomController _phantomController;
-
-  AppFlowyBoard({
+  const AppFlowyBoard({
     required this.controller,
     required this.cardBuilder,
     this.background,
@@ -109,12 +103,7 @@ class AppFlowyBoard extends StatelessWidget {
     this.groupConstraints = const BoxConstraints(maxWidth: 200),
     this.config = const AppFlowyBoardConfig(),
     Key? key,
-  }) : super(key: key) {
-    _phantomController = BoardPhantomController(
-      delegate: controller,
-      groupsState: _groupState,
-    );
-  }
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -122,8 +111,14 @@ class AppFlowyBoard extends StatelessWidget {
       value: controller,
       child: Consumer<AppFlowyBoardController>(
         builder: (context, notifier, child) {
+          final boardState = AppFlowyBoardState();
+          BoardPhantomController phantomController = BoardPhantomController(
+            delegate: controller,
+            groupsState: boardState,
+          );
+
           if (boardScrollController != null) {
-            boardScrollController!._groupState = _groupState;
+            boardScrollController!._boardState = boardState;
           }
 
           return _AppFlowyBoardContent(
@@ -131,14 +126,14 @@ class AppFlowyBoard extends StatelessWidget {
             dataController: controller,
             scrollController: scrollController,
             scrollManager: boardScrollController,
-            groupState: _groupState,
+            boardState: boardState,
             background: background,
-            delegate: _phantomController,
+            delegate: phantomController,
             groupConstraints: groupConstraints,
             cardBuilder: cardBuilder,
             footerBuilder: footerBuilder,
             headerBuilder: headerBuilder,
-            phantomController: _phantomController,
+            phantomController: phantomController,
             onReorder: controller.moveGroup,
           );
         },
@@ -156,7 +151,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
   final ReorderFlexConfig reorderFlexConfig;
   final BoxConstraints groupConstraints;
   final AppFlowyBoardScrollController? scrollManager;
-  final AppFlowyBoardState groupState;
+  final AppFlowyBoardState boardState;
   final AppFlowyBoardCardBuilder cardBuilder;
   final AppFlowyBoardHeaderBuilder? headerBuilder;
   final AppFlowyBoardFooterBuilder? footerBuilder;
@@ -169,7 +164,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
     required this.delegate,
     required this.dataController,
     required this.scrollManager,
-    required this.groupState,
+    required this.boardState,
     this.scrollController,
     this.background,
     required this.groupConstraints,
@@ -178,7 +173,10 @@ class _AppFlowyBoardContent extends StatefulWidget {
     this.headerBuilder,
     required this.phantomController,
     Key? key,
-  })  : reorderFlexConfig = const ReorderFlexConfig(),
+  })  : reorderFlexConfig = const ReorderFlexConfig(
+          direction: Axis.horizontal,
+          dragDirection: Axis.horizontal,
+        ),
         super(key: key);
 
   @override
@@ -198,7 +196,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           reorderFlexId: widget.dataController.identifier,
           acceptedReorderFlexId: widget.dataController.groupIds,
           delegate: widget.delegate,
-          columnsState: widget.groupState,
+          columnsState: widget.boardState,
         );
 
         final reorderFlex = ReorderFlex(
@@ -206,9 +204,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           scrollController: widget.scrollController,
           onReorder: widget.onReorder,
           dataSource: widget.dataController,
-          direction: Axis.horizontal,
           interceptor: interceptor,
-          reorderable: true,
           children: _buildColumns(),
         );
 
@@ -254,7 +250,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
         );
 
         final reorderFlexAction = ReorderFlexActionImpl();
-        widget.groupState.reorderFlexActionMap[columnData.id] =
+        widget.boardState.reorderFlexActionMap[columnData.id] =
             reorderFlexAction;
 
         return ChangeNotifierProvider.value(
@@ -275,8 +271,8 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
                 onReorder: widget.dataController.moveGroupItem,
                 cornerRadius: widget.config.cornerRadius,
                 backgroundColor: widget.config.groupBackgroundColor,
-                dragStateStorage: widget.groupState,
-                dragTargetKeys: widget.groupState,
+                dragStateStorage: widget.boardState,
+                dragTargetKeys: widget.boardState,
                 reorderFlexAction: reorderFlexAction,
               );
 

+ 17 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -138,7 +138,11 @@ class AppFlowyBoardController extends ChangeNotifier
   /// groups or get ready to reinitialize the [AppFlowyBoard].
   void clear() {
     _groupDatas.clear();
+    for (final group in _groupControllers.values) {
+      group.dispose();
+    }
     _groupControllers.clear();
+
     notifyListeners();
   }
 
@@ -202,6 +206,14 @@ class AppFlowyBoardController extends ChangeNotifier
     getGroupController(groupId)?.replaceOrInsertItem(item);
   }
 
+  void enableGroupDragging(bool isEnable) {
+    for (var groupController in _groupControllers.values) {
+      groupController.enableDragging(isEnable);
+    }
+
+    notifyListeners();
+  }
+
   /// Moves the item at [fromGroupIndex] in group with id [fromGroupId] to
   /// group with id [toGroupId] at [toGroupIndex]
   @override
@@ -215,6 +227,8 @@ class AppFlowyBoardController extends ChangeNotifier
     final fromGroupController = getGroupController(fromGroupId)!;
     final toGroupController = getGroupController(toGroupId)!;
     final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
+    if (fromGroupItem == null) return;
+
     if (toGroupController.items.length > toGroupIndex) {
       assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
 
@@ -275,7 +289,9 @@ class AppFlowyBoardController extends ChangeNotifier
         Log.trace(
             '[$BoardPhantomController] update $groupId:$index to $groupId:$newIndex');
         final item = groupController.removeAt(index, notify: false);
-        groupController.insert(newIndex, item, notify: false);
+        if (item != null) {
+          groupController.insert(newIndex, item, notify: false);
+        }
       }
     }
   }

+ 2 - 2
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart

@@ -156,9 +156,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
             widget.onDragStarted?.call(index);
           },
           onReorder: ((fromIndex, toIndex) {
-            if (widget.phantomController.isFromGroup(widget.groupId)) {
+            if (widget.phantomController.shouldReorder(widget.groupId)) {
               widget.onReorder(widget.groupId, fromIndex, toIndex);
-              widget.phantomController.transformIndex(fromIndex, toIndex);
+              widget.phantomController.updateIndex(fromIndex, toIndex);
             }
           }),
           onDragEnded: () {

+ 29 - 4
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart

@@ -5,6 +5,8 @@ import 'package:appflowy_board/src/widgets/reorder_flex/reorder_flex.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flutter/material.dart';
 
+typedef IsDraggable = bool;
+
 /// A item represents the generic data model of each group card.
 ///
 /// Each item displayed in the group required to implement this class.
@@ -50,8 +52,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
   /// * [notify] the default value of [notify] is true, it will notify the
   /// listener. Set to false if you do not want to notify the listeners.
   ///
-  AppFlowyGroupItem removeAt(int index, {bool notify = true}) {
-    assert(index >= 0);
+  AppFlowyGroupItem? removeAt(int index, {bool notify = true}) {
+    if (groupData._items.length <= index) {
+      Log.error(
+          'Fatal error, index is out of bounds. Index: $index,  len: ${groupData._items.length}');
+      return null;
+    }
+
+    if (index < 0) {
+      Log.error('Invalid index:$index');
+      return null;
+    }
 
     Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
     final item = groupData._items.removeAt(index);
@@ -71,12 +82,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
   /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
   /// [fromIndex] equal to the [toIndex].
   bool move(int fromIndex, int toIndex) {
-    assert(fromIndex >= 0);
     assert(toIndex >= 0);
+    if (groupData._items.length < fromIndex) {
+      Log.error(
+          'Out of bounds error. index: $fromIndex should not greater than ${groupData._items.length}');
+      return false;
+    }
 
     if (fromIndex == toIndex) {
       return false;
     }
+
     Log.debug(
         '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
     final item = groupData._items.removeAt(fromIndex);
@@ -124,7 +140,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
       Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
     } else {
       if (index >= groupData._items.length) {
-        Log.warn(
+        Log.error(
             '[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
         return;
       }
@@ -155,6 +171,15 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
         -1;
   }
 
+  void enableDragging(bool isEnable) {
+    groupData.draggable = isEnable;
+
+    for (var item in groupData._items) {
+      item.draggable = isEnable;
+    }
+    _notify();
+  }
+
   void _notify() {
     notifyListeners();
   }

+ 6 - 6
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart

@@ -16,13 +16,13 @@ class FlexDragTargetData extends DragTargetData {
   @override
   final int draggingIndex;
 
-  final DraggingState _state;
+  final DraggingState _draggingState;
 
-  Widget? get draggingWidget => _state.draggingWidget;
+  Widget? get draggingWidget => _draggingState.draggingWidget;
 
-  Size? get feedbackSize => _state.feedbackSize;
+  Size? get feedbackSize => _draggingState.feedbackSize;
 
-  bool get isDragging => _state.isDragging();
+  bool get isDragging => _draggingState.isDragging();
 
   final String dragTargetId;
 
@@ -40,8 +40,8 @@ class FlexDragTargetData extends DragTargetData {
     required this.reorderFlexId,
     required this.reorderFlexItem,
     required this.dragTargetIndexKey,
-    required DraggingState state,
-  }) : _state = state;
+    required DraggingState draggingState,
+  }) : _draggingState = draggingState;
 
   @override
   String toString() {

+ 11 - 7
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_board/appflowy_board.dart';
 import 'package:appflowy_board/src/utils/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
@@ -78,10 +79,12 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
 
   final bool useMoveAnimation;
 
-  final bool draggable;
+  final IsDraggable draggable;
 
   final double draggingOpacity;
 
+  final Axis? dragDirection;
+
   const ReorderDragTarget({
     Key? key,
     required this.child,
@@ -99,6 +102,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
     this.onLeave,
     this.draggableTargetBuilder,
     this.draggingOpacity = 0.3,
+    this.dragDirection,
   }) : super(key: key);
 
   @override
@@ -115,8 +119,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
     Widget dragTarget = DragTarget<T>(
       builder: _buildDraggableWidget,
       onWillAccept: (dragTargetData) {
-        assert(dragTargetData != null);
-        if (dragTargetData == null) return false;
+        if (dragTargetData == null) {
+          return false;
+        }
+
         return widget.onWillAccept(dragTargetData);
       },
       onAccept: widget.onAccept,
@@ -140,9 +146,6 @@ class _ReorderDragTargetState<T extends DragTargetData>
     List<T?> acceptedCandidates,
     List<dynamic> rejectedCandidates,
   ) {
-    if (!widget.draggable) {
-      return widget.child;
-    }
     Widget feedbackBuilder = Builder(builder: (BuildContext context) {
       BoxConstraints contentSizeConstraints =
           BoxConstraints.loose(_draggingFeedbackSize!);
@@ -163,7 +166,8 @@ class _ReorderDragTargetState<T extends DragTargetData>
           widget.deleteAnimationController,
         ) ??
         Draggable<DragTargetData>(
-          maxSimultaneousDrags: 1,
+          axis: widget.dragDirection,
+          maxSimultaneousDrags: widget.draggable ? 1 : 0,
           data: widget.dragTargetData,
           ignoringFeedbackSemantics: false,
           feedback: feedbackBuilder,

+ 68 - 55
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -1,6 +1,7 @@
 import 'dart:collection';
 import 'dart:math';
 
+import 'package:appflowy_board/appflowy_board.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 import '../../utils/log.dart';
@@ -29,6 +30,8 @@ abstract class ReoderFlexDataSource {
 abstract class ReoderFlexItem {
   /// [id] is used to identify the item. It must be unique.
   String get id;
+
+  IsDraggable draggable = true;
 }
 
 /// Cache each dragTarget's key.
@@ -73,8 +76,15 @@ class ReorderFlexConfig {
 
   final bool useMovePlaceholder;
 
+  /// [direction] How to place the children, default is Axis.vertical
+  final Axis direction;
+
+  final Axis? dragDirection;
+
   const ReorderFlexConfig({
     this.useMoveAnimation = true,
+    this.direction = Axis.vertical,
+    this.dragDirection,
   }) : useMovePlaceholder = !useMoveAnimation;
 }
 
@@ -82,8 +92,6 @@ class ReorderFlex extends StatefulWidget {
   final ReorderFlexConfig config;
   final List<Widget> children;
 
-  /// [direction] How to place the children, default is Axis.vertical
-  final Axis direction;
   final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
 
   final ScrollController? scrollController;
@@ -108,8 +116,6 @@ class ReorderFlex extends StatefulWidget {
 
   final ReorderFlexAction? reorderFlexAction;
 
-  final bool reorderable;
-
   ReorderFlex({
     Key? key,
     this.scrollController,
@@ -117,14 +123,12 @@ class ReorderFlex extends StatefulWidget {
     required this.children,
     required this.config,
     required this.onReorder,
-    this.reorderable = true,
     this.dragStateStorage,
     this.dragTargetKeys,
     this.onDragStarted,
     this.onDragEnded,
     this.interceptor,
     this.reorderFlexAction,
-    this.direction = Axis.vertical,
   })  : assert(children.every((Widget w) => w.key != null),
             'All child must have a key.'),
         super(key: key);
@@ -146,8 +150,8 @@ class ReorderFlexState extends State<ReorderFlex>
   /// Whether or not we are currently scrolling this view to show a widget.
   bool _scrolling = false;
 
-  /// [dragState] records the dragging state including dragStartIndex, and phantomIndex, etc.
-  late DraggingState dragState;
+  /// [draggingState] records the dragging state including dragStartIndex, and phantomIndex, etc.
+  late DraggingState draggingState;
 
   /// [_animation] controls the dragging animations
   late DragTargetAnimation _animation;
@@ -158,9 +162,9 @@ class ReorderFlexState extends State<ReorderFlex>
   void initState() {
     _notifier = ReorderFlexNotifier();
     final flexId = widget.reorderFlexId;
-    dragState = widget.dragStateStorage?.readState(flexId) ??
+    draggingState = widget.dragStateStorage?.readState(flexId) ??
         DraggingState(widget.reorderFlexId);
-    Log.trace('[DragTarget] init dragState: $dragState');
+    Log.trace('[DragTarget] init dragState: $draggingState');
 
     widget.dragStateStorage?.removeState(flexId);
 
@@ -168,7 +172,7 @@ class ReorderFlexState extends State<ReorderFlex>
       reorderAnimationDuration: widget.config.reorderAnimationDuration,
       entranceAnimateStatusChanged: (status) {
         if (status == AnimationStatus.completed) {
-          if (dragState.nextIndex == -1) return;
+          if (draggingState.nextIndex == -1) return;
           setState(() => _requestAnimationToNextIndex());
         }
       },
@@ -225,7 +229,7 @@ class ReorderFlexState extends State<ReorderFlex>
         indexKey,
       );
 
-      children.add(_wrap(child, i, indexKey));
+      children.add(_wrap(child, i, indexKey, item.draggable));
 
       // if (widget.config.useMovePlaceholder) {
       //   children.add(DragTargeMovePlaceholder(
@@ -256,64 +260,70 @@ class ReorderFlexState extends State<ReorderFlex>
     /// when the animation finish.
 
     if (_animation.entranceController.isCompleted) {
-      dragState.removePhantom();
+      draggingState.removePhantom();
 
-      if (!isAcceptingNewTarget && dragState.didDragTargetMoveToNext()) {
+      if (!isAcceptingNewTarget && draggingState.didDragTargetMoveToNext()) {
         return;
       }
 
-      dragState.moveDragTargetToNext();
+      draggingState.moveDragTargetToNext();
       _animation.animateToNext();
     }
   }
 
   /// [child]: the child will be wrapped with dartTarget
   /// [childIndex]: the index of the child in a list
-  Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
+  Widget _wrap(
+    Widget child,
+    int childIndex,
+    GlobalObjectKey indexKey,
+    IsDraggable draggable,
+  ) {
     return Builder(builder: (context) {
       final ReorderDragTarget dragTarget = _buildDragTarget(
         context,
         child,
         childIndex,
         indexKey,
+        draggable,
       );
       int shiftedIndex = childIndex;
 
-      if (dragState.isOverlapWithPhantom()) {
-        shiftedIndex = dragState.calculateShiftedIndex(childIndex);
+      if (draggingState.isOverlapWithPhantom()) {
+        shiftedIndex = draggingState.calculateShiftedIndex(childIndex);
       }
 
       Log.trace(
-          'Rebuild: Group:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
-      final currentIndex = dragState.currentIndex;
-      final dragPhantomIndex = dragState.phantomIndex;
+          'Rebuild: Group:[${draggingState.reorderFlexId}] ${draggingState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
+      final currentIndex = draggingState.currentIndex;
+      final dragPhantomIndex = draggingState.phantomIndex;
 
       if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) {
         Widget dragSpace;
-        if (dragState.draggingWidget != null) {
-          if (dragState.draggingWidget is PhantomWidget) {
-            dragSpace = dragState.draggingWidget!;
+        if (draggingState.draggingWidget != null) {
+          if (draggingState.draggingWidget is PhantomWidget) {
+            dragSpace = draggingState.draggingWidget!;
           } else {
             dragSpace = PhantomWidget(
               opacity: widget.config.draggingWidgetOpacity,
-              child: dragState.draggingWidget,
+              child: draggingState.draggingWidget,
             );
           }
         } else {
-          dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize);
+          dragSpace = SizedBox.fromSize(size: draggingState.dropAreaSize);
         }
 
         /// Returns the dragTarget it is not start dragging. The size of the
         /// dragTarget is the same as the the passed in child.
         ///
-        if (dragState.isNotDragging()) {
+        if (draggingState.isNotDragging()) {
           return _buildDraggingContainer(children: [dragTarget]);
         }
 
         /// Determine the size of the drop area to show under the dragging widget.
         Size? feedbackSize = Size.zero;
         if (widget.config.useMoveAnimation) {
-          feedbackSize = dragState.feedbackSize;
+          feedbackSize = draggingState.feedbackSize;
         }
 
         Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
@@ -321,7 +331,7 @@ class ReorderFlexState extends State<ReorderFlex>
 
         /// When start dragging, the dragTarget, [ReorderDragTarget], will
         /// return a [IgnorePointerWidget] which size is zero.
-        if (dragState.isPhantomAboveDragTarget()) {
+        if (draggingState.isPhantomAboveDragTarget()) {
           _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
@@ -343,7 +353,7 @@ class ReorderFlexState extends State<ReorderFlex>
         }
 
         ///
-        if (dragState.isPhantomBelowDragTarget()) {
+        if (draggingState.isPhantomBelowDragTarget()) {
           _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
@@ -364,10 +374,10 @@ class ReorderFlexState extends State<ReorderFlex>
           }
         }
 
-        assert(!dragState.isOverlapWithPhantom());
+        assert(!draggingState.isOverlapWithPhantom());
 
         List<Widget> children = [];
-        if (dragState.isDragTargetMovingDown()) {
+        if (draggingState.isDragTargetMovingDown()) {
           children.addAll([dragTarget, appearSpace]);
         } else {
           children.addAll([appearSpace, dragTarget]);
@@ -395,15 +405,17 @@ class ReorderFlexState extends State<ReorderFlex>
     Widget child,
     int dragTargetIndex,
     GlobalObjectKey indexKey,
+    IsDraggable draggable,
   ) {
     final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
     return ReorderDragTarget<FlexDragTargetData>(
       indexGlobalKey: indexKey,
+      draggable: draggable,
       dragTargetData: FlexDragTargetData(
         draggingIndex: dragTargetIndex,
         reorderFlexId: widget.reorderFlexId,
         reorderFlexItem: reorderFlexItem,
-        state: dragState,
+        draggingState: draggingState,
         dragTargetId: reorderFlexItem.id,
         dragTargetIndexKey: indexKey,
       ),
@@ -432,11 +444,11 @@ class ReorderFlexState extends State<ReorderFlex>
         setState(() {
           if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
             _onReordered(
-              dragState.dragStartIndex,
-              dragState.currentIndex,
+              draggingState.dragStartIndex,
+              draggingState.currentIndex,
             );
           }
-          dragState.endDragging();
+          draggingState.endDragging();
           widget.onDragEnded?.call();
         });
       },
@@ -482,8 +494,8 @@ class ReorderFlexState extends State<ReorderFlex>
       deleteAnimationController: _animation.deleteController,
       draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
       useMoveAnimation: widget.config.useMoveAnimation,
-      draggable: widget.reorderable,
       draggingOpacity: widget.config.draggingWidgetOpacity,
+      dragDirection: widget.config.dragDirection,
       child: child,
     );
   }
@@ -506,7 +518,7 @@ class ReorderFlexState extends State<ReorderFlex>
       child,
       _animation.entranceController,
       feedbackSize,
-      widget.direction,
+      widget.config.direction,
     );
   }
 
@@ -515,7 +527,7 @@ class ReorderFlexState extends State<ReorderFlex>
       child,
       _animation.phantomController,
       feedbackSize,
-      widget.direction,
+      widget.config.direction,
     );
   }
 
@@ -525,7 +537,7 @@ class ReorderFlexState extends State<ReorderFlex>
     Size? feedbackSize,
   ) {
     setState(() {
-      dragState.startDragging(draggingWidget, dragIndex, feedbackSize);
+      draggingState.startDragging(draggingWidget, dragIndex, feedbackSize);
       _animation.startDragging();
     });
   }
@@ -535,34 +547,34 @@ class ReorderFlexState extends State<ReorderFlex>
       return;
     }
 
-    dragState.setStartDraggingIndex(dragTargetIndex);
+    draggingState.setStartDraggingIndex(dragTargetIndex);
     widget.dragStateStorage?.insertState(
       widget.reorderFlexId,
-      dragState,
+      draggingState,
     );
   }
 
   bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
-    final dragIndex = dragState.dragStartIndex;
+    final dragIndex = draggingState.dragStartIndex;
 
     /// The [willAccept] will be true if the dargTarget is the widget that gets
     /// dragged and it is dragged on top of the other dragTargets.
     ///
 
-    bool willAccept =
-        dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex;
+    bool willAccept = draggingState.dragStartIndex == dragIndex &&
+        dragIndex != dragTargetIndex;
     setState(() {
       if (willAccept) {
-        int shiftedIndex = dragState.calculateShiftedIndex(dragTargetIndex);
-        dragState.updateNextIndex(shiftedIndex);
+        int shiftedIndex = draggingState.calculateShiftedIndex(dragTargetIndex);
+        draggingState.updateNextIndex(shiftedIndex);
       } else {
-        dragState.updateNextIndex(dragTargetIndex);
+        draggingState.updateNextIndex(dragTargetIndex);
       }
       _requestAnimationToNextIndex(isAcceptingNewTarget: true);
     });
 
     Log.trace(
-        '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}');
+        '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $draggingState}');
 
     _scrollTo(context);
 
@@ -587,7 +599,7 @@ class ReorderFlexState extends State<ReorderFlex>
       return child;
     } else {
       return SingleChildScrollView(
-        scrollDirection: widget.direction,
+        scrollDirection: widget.config.direction,
         controller: _scrollController,
         child: child,
       );
@@ -595,7 +607,7 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   Widget _wrapContainer(List<Widget> children) {
-    switch (widget.direction) {
+    switch (widget.config.direction) {
       case Axis.horizontal:
         return Row(
           crossAxisAlignment: CrossAxisAlignment.start,
@@ -613,7 +625,7 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   Widget _buildDraggingContainer({required List<Widget> children}) {
-    switch (widget.direction) {
+    switch (widget.config.direction) {
       case Axis.horizontal:
         return Row(
           crossAxisAlignment: CrossAxisAlignment.start,
@@ -660,6 +672,7 @@ class ReorderFlexState extends State<ReorderFlex>
             .ensureVisible(
           dragTargetRenderObject,
           alignment: 0.5,
+          alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
           duration: const Duration(milliseconds: 120),
         )
             .then((value) {
@@ -683,9 +696,9 @@ class ReorderFlexState extends State<ReorderFlex>
     // If and only if the current scroll offset falls in-between the offsets
     // necessary to reveal the selected context at the top or bottom of the
     // screen, then it is already on-screen.
-    final double margin = widget.direction == Axis.horizontal
-        ? dragState.dropAreaSize.width
-        : dragState.dropAreaSize.height / 2.0;
+    final double margin = widget.config.direction == Axis.horizontal
+        ? draggingState.dropAreaSize.width
+        : draggingState.dropAreaSize.height / 2.0;
     if (_scrollController.hasClients) {
       final double scrollOffset = _scrollController.offset;
       final double topOffset = max(

+ 11 - 8
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart

@@ -46,15 +46,23 @@ class BoardPhantomController extends OverlapDragTargetDelegate
     required this.groupsState,
   });
 
-  bool isFromGroup(String groupId) {
+  /// Determines whether the group should perform reorder
+  ///
+  /// Returns `true` if the fromGroupId and toGroupId of the phantomRecord
+  /// equal to the passed in groupId.
+  ///
+  /// Returns `true` if the phantomRecord is null
+  ///
+  bool shouldReorder(String groupId) {
     if (phantomRecord != null) {
-      return phantomRecord!.fromGroupId == groupId;
+      return phantomRecord!.toGroupId == groupId &&
+          phantomRecord!.fromGroupId == groupId;
     } else {
       return true;
     }
   }
 
-  void transformIndex(int fromIndex, int toIndex) {
+  void updateIndex(int fromIndex, int toIndex) {
     if (phantomRecord == null) {
       return;
     }
@@ -69,7 +77,6 @@ class BoardPhantomController extends OverlapDragTargetDelegate
   /// Remove the phantom in the group when the group is end dragging.
   void groupEndDragging(String groupId) {
     phantomState.setGroupIsDragging(groupId, false);
-
     if (phantomRecord == null) return;
 
     final fromGroupId = phantomRecord!.fromGroupId;
@@ -246,10 +253,6 @@ class PhantomRecord {
   });
 
   void updateFromGroupIndex(int index) {
-    if (fromGroupIndex == index) {
-      return;
-    }
-
     fromGroupIndex = index;
   }
 

+ 1 - 1
frontend/app_flowy/pubspec.lock

@@ -28,7 +28,7 @@ packages:
       path: "packages/appflowy_board"
       relative: true
     source: path
-    version: "0.0.7"
+    version: "0.0.8"
   appflowy_editor:
     dependency: "direct main"
     description:

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -623,7 +623,7 @@ impl GridRevisionEditor {
                 self.view_manager
                     .move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| {
                         wrap_future(async move {
-                            tracing::trace!("Move group row cause row data changed: {:?}", row_changeset);
+                            tracing::trace!("Row data changed: {:?}", row_changeset);
                             let cell_changesets = row_changeset
                                 .cell_by_field_id
                                 .into_iter()

+ 13 - 13
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -79,7 +79,7 @@ impl GridViewRevisionEditor {
         Ok(json_str)
     }
 
-    pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
+    pub(crate) async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         if params.group_id.is_none() {
             return;
         }
@@ -92,7 +92,7 @@ impl GridViewRevisionEditor {
             .await;
     }
 
-    pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
+    pub(crate) async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
         // Send the group notification if the current view has groups
         match params.group_id.as_ref() {
             None => {}
@@ -115,7 +115,7 @@ impl GridViewRevisionEditor {
     }
 
     #[tracing::instrument(level = "trace", skip_all)]
-    pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
+    pub(crate) async fn did_delete_view_row(&self, row_rev: &RowRevision) {
         // Send the group notification if the current view has groups;
         let changesets = self
             .mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev))
@@ -129,7 +129,7 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) {
+    pub(crate) async fn did_update_view_row(&self, row_rev: &RowRevision) {
         let changesets = self
             .mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev))
             .await;
@@ -141,7 +141,7 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn move_group_row(
+    pub(crate) async fn move_view_group_row(
         &self,
         row_rev: &RowRevision,
         row_changeset: &mut RowChangeset,
@@ -167,14 +167,14 @@ impl GridViewRevisionEditor {
     }
     /// Only call once after grid view editor initialized
     #[tracing::instrument(level = "trace", skip(self))]
-    pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
+    pub(crate) async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
         let groups = self.group_controller.read().await.groups();
         tracing::trace!("Number of groups: {}", groups.len());
         Ok(groups.into_iter().map(GroupPB::from).collect())
     }
 
     #[tracing::instrument(level = "trace", skip(self), err)]
-    pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
+    pub(crate) async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let _ = self
             .group_controller
             .write()
@@ -206,13 +206,13 @@ impl GridViewRevisionEditor {
         self.group_controller.read().await.field_id().to_owned()
     }
 
-    pub(crate) async fn get_setting(&self) -> GridSettingPB {
+    pub(crate) async fn get_view_setting(&self) -> GridSettingPB {
         let field_revs = self.field_delegate.get_field_revs().await;
         let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs);
         grid_setting
     }
 
-    pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfigurationPB> {
+    pub(crate) async fn get_view_filters(&self) -> Vec<GridFilterConfigurationPB> {
         let field_revs = self.field_delegate.get_field_revs().await;
         match self.pad.read().await.get_all_filters(&field_revs) {
             None => vec![],
@@ -245,7 +245,7 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
-    pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+    pub(crate) async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
         self.modify(|pad| {
             let changeset = pad.delete_filter(&params.field_id, &params.field_type_rev, &params.group_id)?;
             Ok(changeset)
@@ -253,7 +253,7 @@ impl GridViewRevisionEditor {
         .await
     }
 
-    pub(crate) async fn insert_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
+    pub(crate) async fn insert_view_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
         self.modify(|pad| {
             let filter_rev = FilterConfigurationRevision {
                 id: gen_grid_filter_id(),
@@ -267,7 +267,7 @@ impl GridViewRevisionEditor {
         .await
     }
 
-    pub(crate) async fn delete_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> {
+    pub(crate) async fn delete_view_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> {
         self.modify(|pad| {
             let changeset = pad.delete_filter(
                 &delete_filter.field_id,
@@ -324,7 +324,7 @@ impl GridViewRevisionEditor {
     }
 
     async fn notify_did_update_setting(&self) {
-        let setting = self.get_setting().await;
+        let setting = self.get_view_setting().await;
         send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
             .payload(setting)
             .send();

+ 12 - 12
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

@@ -65,14 +65,14 @@ impl GridViewManager {
     /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
     pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
-            view_editor.will_create_row(row_rev, params).await;
+            view_editor.will_create_view_row(row_rev, params).await;
         }
     }
 
     /// Notify the view that the row was created. For the moment, the view is just sending notifications.
     pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
-            view_editor.did_create_row(row_pb, params).await;
+            view_editor.did_create_view_row(row_pb, params).await;
         }
     }
 
@@ -84,7 +84,7 @@ impl GridViewManager {
             }
             Some(row_rev) => {
                 for view_editor in self.view_editors.iter() {
-                    view_editor.did_update_row(&row_rev).await;
+                    view_editor.did_update_view_row(&row_rev).await;
                 }
             }
         }
@@ -102,33 +102,33 @@ impl GridViewManager {
 
     pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
         for view_editor in self.view_editors.iter() {
-            view_editor.did_delete_row(&row_rev).await;
+            view_editor.did_delete_view_row(&row_rev).await;
         }
     }
 
     pub(crate) async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
         let view_editor = self.get_default_view_editor().await?;
-        Ok(view_editor.get_setting().await)
+        Ok(view_editor.get_view_setting().await)
     }
 
     pub(crate) async fn get_filters(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> {
         let view_editor = self.get_default_view_editor().await?;
-        Ok(view_editor.get_filters().await)
+        Ok(view_editor.get_view_filters().await)
     }
 
     pub(crate) async fn insert_or_update_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        view_editor.insert_filter(params).await
+        view_editor.insert_view_filter(params).await
     }
 
     pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        view_editor.delete_filter(params).await
+        view_editor.delete_view_filter(params).await
     }
 
     pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
         let view_editor = self.get_default_view_editor().await?;
-        let groups = view_editor.load_groups().await?;
+        let groups = view_editor.load_view_groups().await?;
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
@@ -139,12 +139,12 @@ impl GridViewManager {
 
     pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        view_editor.delete_group(params).await
+        view_editor.delete_view_group(params).await
     }
 
     pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        let _ = view_editor.move_group(params).await?;
+        let _ = view_editor.move_view_group(params).await?;
         Ok(())
     }
 
@@ -161,7 +161,7 @@ impl GridViewManager {
         let mut row_changeset = RowChangeset::new(row_rev.id.clone());
         let view_editor = self.get_default_view_editor().await?;
         let group_changesets = view_editor
-            .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
+            .move_view_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
             .await;
 
         if !row_changeset.is_empty() {

+ 9 - 1
frontend/rust-lib/flowy-grid/src/services/group/configuration.rs

@@ -119,12 +119,20 @@ where
                 self.mut_configuration(|configuration| {
                     let from_index = configuration.groups.iter().position(|group| group.id == from_id);
                     let to_index = configuration.groups.iter().position(|group| group.id == to_id);
-                    tracing::info!("Configuration groups: {:?} ", configuration.groups);
                     if let (Some(from), Some(to)) = &(from_index, to_index) {
                         tracing::trace!("Move group from index:{:?} to index:{:?}", from_index, to_index);
                         let group = configuration.groups.remove(*from);
                         configuration.groups.insert(*to, group);
                     }
+                    tracing::debug!(
+                        "Group order: {:?} ",
+                        configuration
+                            .groups
+                            .iter()
+                            .map(|group| group.name.clone())
+                            .collect::<Vec<String>>()
+                            .join(",")
+                    );
 
                     from_index.is_some() && to_index.is_some()
                 })?;

+ 4 - 3
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs

@@ -80,9 +80,9 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
     };
 
     // Remove the row in which group contains it
-    if from_index.is_some() {
+    if let Some(from_index) = &from_index {
         changeset.deleted_rows.push(row_rev.id.clone());
-        tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id);
+        tracing::debug!("Group:{} remove {} at {}", group.id, row_rev.id, from_index);
         group.remove_row(&row_rev.id);
     }
 
@@ -97,10 +97,11 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
             }
             Some(to_index) => {
                 if to_index < group.number_of_row() {
-                    tracing::debug!("Group:{} insert row:{} at {} ", group.id, row_rev.id, to_index);
+                    tracing::debug!("Group:{} insert {} at {} ", group.id, row_rev.id, to_index);
                     inserted_row.index = Some(to_index as i32);
                     group.insert_row(to_index, row_pb);
                 } else {
+                    tracing::warn!("Mote to index: {} is out of bounds", to_index);
                     tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
                     group.add_row(row_pb);
                 }

+ 44 - 0
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -358,10 +358,18 @@ async fn group_move_group_test() {
             from_group_index: 0,
             to_group_index: 1,
         },
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 2,
+        },
         AssertGroup {
             group_index: 0,
             expected_group: group_1,
         },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
         AssertGroup {
             group_index: 1,
             expected_group: group_0,
@@ -370,6 +378,42 @@ async fn group_move_group_test() {
     test.run_scripts(scripts).await;
 }
 
+#[tokio::test]
+async fn group_move_group_row_after_move_group_test() {
+    let mut test = GridGroupTest::new().await;
+    let group_0 = test.group_at_index(0).await;
+    let group_1 = test.group_at_index(1).await;
+    let scripts = vec![
+        MoveGroup {
+            from_group_index: 0,
+            to_group_index: 1,
+        },
+        AssertGroup {
+            group_index: 0,
+            expected_group: group_1,
+        },
+        AssertGroup {
+            group_index: 1,
+            expected_group: group_0,
+        },
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 1,
+            to_row_index: 0,
+        },
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 1,
+        },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 3,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
 #[tokio::test]
 async fn group_default_move_group_test() {
     let mut test = GridGroupTest::new().await;