Переглянути джерело

Merge pull request #1197 from AppFlowy-IO/feat/support_drag_group

Feat/support drag group
Nathan.fooo 2 роки тому
батько
коміт
f355ff01e4
28 змінених файлів з 321 додано та 240 видалено
  1. 7 7
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  2. 2 10
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  3. 4 3
      frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
  4. 1 1
      frontend/app_flowy/packages/appflowy_board/example/lib/main.dart
  5. 7 0
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  6. 36 52
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  7. 7 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart
  8. 12 8
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart
  9. 3 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart
  10. 20 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  11. 2 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart
  12. 75 26
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  13. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart
  14. 1 1
      frontend/app_flowy/packages/appflowy_board/pubspec.yaml
  15. 4 4
      frontend/rust-lib/Cargo.lock
  16. 1 1
      frontend/rust-lib/flowy-grid/Cargo.toml
  17. 1 0
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs
  18. 1 0
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  19. 12 6
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  20. 45 62
      frontend/rust-lib/flowy-grid/src/services/group/configuration.rs
  21. 26 20
      frontend/rust-lib/flowy-grid/src/services/group/controller.rs
  22. 3 2
      frontend/rust-lib/flowy-grid/src/services/group/entities.rs
  23. 21 3
      frontend/rust-lib/flowy-grid/src/services/group/group_util.rs
  24. 23 1
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs
  25. 4 4
      shared-lib/Cargo.lock
  26. 1 1
      shared-lib/flowy-grid-data-model/Cargo.toml
  27. 0 8
      shared-lib/flowy-grid-data-model/src/revision/group_rev.rs
  28. 1 1
      shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs

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

@@ -36,21 +36,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         super(BoardState.initial(view.id)) {
     boardController = AppFlowyBoardController(
       onMoveGroup: (
-        fromColumnId,
+        fromGroupId,
         fromIndex,
-        toColumnId,
+        toGroupId,
         toIndex,
       ) {
-        _moveGroup(fromColumnId, toColumnId);
+        _moveGroup(fromGroupId, toGroupId);
       },
       onMoveGroupItem: (
-        columnId,
+        groupId,
         fromIndex,
         toIndex,
       ) {
-        final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex);
-        final toRow = groupControllers[columnId]?.rowAtIndex(toIndex);
-        _moveRow(fromRow, columnId, toRow);
+        final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, groupId, toRow);
       },
       onMoveGroupItemToGroup: (
         fromGroupId,

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

@@ -69,7 +69,6 @@ class BoardContent extends StatefulWidget {
 
 class _BoardContentState extends State<BoardContent> {
   late AppFlowyBoardScrollController scrollManager;
-  final Map<String, ValueKey> cardKeysCache = {};
 
   final config = AppFlowyBoardConfig(
     groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
@@ -139,7 +138,7 @@ class _BoardContentState extends State<BoardContent> {
                 .read<BoardBloc>()
                 .add(BoardEvent.endEditRow(editingRow.row.id));
           } else {
-            scrollManager.scrollToBottom(editingRow.columnId, () {
+            scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
               context
                   .read<BoardBloc>()
                   .add(BoardEvent.endEditRow(editingRow.row.id));
@@ -247,15 +246,8 @@ class _BoardContentState extends State<BoardContent> {
     );
 
     final groupItemId = columnItem.id + group.id;
-    ValueKey? key = cardKeysCache[groupItemId];
-    if (key == null) {
-      final newKey = ValueKey(groupItemId);
-      cardKeysCache[groupItemId] = newKey;
-      key = newKey;
-    }
-
     return AppFlowyGroupCard(
-      key: key,
+      key: ValueKey(groupItemId),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
       child: BoardCard(

+ 4 - 3
frontend/app_flowy/packages/appflowy_board/CHANGELOG.md

@@ -1,3 +1,5 @@
+# 0.0.8
+* Enable drag and drop group  
 # 0.0.7
 * Rename some classes
 * Add documentation
@@ -7,7 +9,7 @@
 
 # 0.0.5
 * Optimize insert card animation
-* Enable insert card at the end of the column
+* Enable insert card at the end of the group 
 * Fix some bugs
 
 # 0.0.4
@@ -24,6 +26,5 @@
 
 # 0.0.1
 
-* Support drag and drop column
-* Support drag and drop column items from one to another
+* Support drag and drop group items from one to another
 

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

@@ -34,7 +34,7 @@ class _MyAppState extends State<MyApp> {
           appBar: AppBar(
             title: const Text('AppFlowy Board'),
           ),
-          body: _examples[_currentIndex],
+          body: Container(color: Colors.white, child: _examples[_currentIndex]),
           bottomNavigationBar: BottomNavigationBar(
             fixedColor: _bottomNavigationColor,
             showSelectedLabels: true,

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

@@ -21,8 +21,11 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
     },
   );
 
+  late AppFlowyBoardScrollController boardController;
+
   @override
   void initState() {
+    boardController = AppFlowyBoardScrollController();
     final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [
       TextItem("Card 1"),
       TextItem("Card 2"),
@@ -67,12 +70,16 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
             child: _buildCard(groupItem),
           );
         },
+        boardScrollController: boardController,
         footerBuilder: (context, columnData) {
           return AppFlowyGroupFooter(
             icon: const Icon(Icons.add, size: 20),
             title: const Text('New'),
             height: 50,
             margin: config.groupItemPadding,
+            onAddButtonClick: () {
+              boardController.scrollToBottom(columnData.id, (p0) {});
+            },
           );
         },
         headerBuilder: (context, columnData) {

+ 36 - 52
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -13,10 +13,8 @@ import '../rendering/board_overlay.dart';
 class AppFlowyBoardScrollController {
   AppFlowyBoardState? _groupState;
 
-  void scrollToBottom(String groupId, VoidCallback? completed) {
-    _groupState
-        ?.getReorderFlexState(groupId: groupId)
-        ?.scrollToBottom(completed);
+  void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
+    _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
   }
 }
 
@@ -133,7 +131,7 @@ class AppFlowyBoard extends StatelessWidget {
             dataController: controller,
             scrollController: scrollController,
             scrollManager: boardScrollController,
-            columnsState: _groupState,
+            groupState: _groupState,
             background: background,
             delegate: _phantomController,
             groupConstraints: groupConstraints,
@@ -158,7 +156,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
   final ReorderFlexConfig reorderFlexConfig;
   final BoxConstraints groupConstraints;
   final AppFlowyBoardScrollController? scrollManager;
-  final AppFlowyBoardState columnsState;
+  final AppFlowyBoardState groupState;
   final AppFlowyBoardCardBuilder cardBuilder;
   final AppFlowyBoardHeaderBuilder? headerBuilder;
   final AppFlowyBoardFooterBuilder? footerBuilder;
@@ -171,7 +169,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
     required this.delegate,
     required this.dataController,
     required this.scrollManager,
-    required this.columnsState,
+    required this.groupState,
     this.scrollController,
     this.background,
     required this.groupConstraints,
@@ -192,8 +190,6 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
       GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key');
   late BoardOverlayEntry _overlayEntry;
 
-  final Map<String, GlobalObjectKey> _reorderFlexKeys = {};
-
   @override
   void initState() {
     _overlayEntry = BoardOverlayEntry(
@@ -202,7 +198,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           reorderFlexId: widget.dataController.identifier,
           acceptedReorderFlexId: widget.dataController.groupIds,
           delegate: widget.delegate,
-          columnsState: widget.columnsState,
+          columnsState: widget.groupState,
         );
 
         final reorderFlex = ReorderFlex(
@@ -212,7 +208,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           dataSource: widget.dataController,
           direction: Axis.horizontal,
           interceptor: interceptor,
-          reorderable: false,
+          reorderable: true,
           children: _buildColumns(),
         );
 
@@ -257,18 +253,16 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           dataController: widget.dataController,
         );
 
-        if (_reorderFlexKeys[columnData.id] == null) {
-          _reorderFlexKeys[columnData.id] = GlobalObjectKey(columnData.id);
-        }
+        final reorderFlexAction = ReorderFlexActionImpl();
+        widget.groupState.reorderFlexActionMap[columnData.id] =
+            reorderFlexAction;
 
-        GlobalObjectKey reorderFlexKey = _reorderFlexKeys[columnData.id]!;
         return ChangeNotifierProvider.value(
           key: ValueKey(columnData.id),
           value: widget.dataController.getGroupController(columnData.id),
           child: Consumer<AppFlowyGroupController>(
             builder: (context, value, child) {
               final boardColumn = AppFlowyBoardGroup(
-                reorderFlexKey: reorderFlexKey,
                 // key: PageStorageKey<String>(columnData.id),
                 margin: _marginFromIndex(columnIndex),
                 itemMargin: widget.config.groupItemPadding,
@@ -281,11 +275,11 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
                 onReorder: widget.dataController.moveGroupItem,
                 cornerRadius: widget.config.cornerRadius,
                 backgroundColor: widget.config.groupBackgroundColor,
-                dragStateStorage: widget.columnsState,
-                dragTargetIndexKeyStorage: widget.columnsState,
+                dragStateStorage: widget.groupState,
+                dragTargetKeys: widget.groupState,
+                reorderFlexAction: reorderFlexAction,
               );
 
-              widget.columnsState.addGroup(columnData.id, boardColumn);
               return ConstrainedBox(
                 constraints: widget.groupConstraints,
                 child: boardColumn,
@@ -356,71 +350,61 @@ class AppFlowyGroupContext {
 }
 
 class AppFlowyBoardState extends DraggingStateStorage
-    with ReorderDragTargetIndexKeyStorage {
-  /// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the
-  /// AppFlowyBoardGroup's [ReorderFlex] widget.
-  final Map<String, GlobalKey> groupReorderFlexKeys = {};
+    with ReorderDragTargeKeys {
   final Map<String, DraggingState> groupDragStates = {};
-  final Map<String, Map<String, GlobalObjectKey>> groupDragDragTargets = {};
-
-  void addGroup(String groupId, AppFlowyBoardGroup groupWidget) {
-    groupReorderFlexKeys[groupId] = groupWidget.reorderFlexKey;
-  }
+  final Map<String, Map<String, GlobalObjectKey>> groupDragTargetKeys = {};
 
-  ReorderFlexState? getReorderFlexState({required String groupId}) {
-    final flexGlobalKey = groupReorderFlexKeys[groupId];
-    if (flexGlobalKey == null) return null;
-    if (flexGlobalKey.currentState is! ReorderFlexState) return null;
-    final state = flexGlobalKey.currentState as ReorderFlexState;
-    return state;
-  }
-
-  ReorderFlex? getReorderFlex({required String groupId}) {
-    final flexGlobalKey = groupReorderFlexKeys[groupId];
-    if (flexGlobalKey == null) return null;
-    if (flexGlobalKey.currentWidget is! ReorderFlex) return null;
-    final widget = flexGlobalKey.currentWidget as ReorderFlex;
-    return widget;
-  }
+  /// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the
+  /// AppFlowyBoardGroup's [ReorderFlex] widget.
+  final Map<String, ReorderFlexActionImpl> reorderFlexActionMap = {};
 
   @override
-  DraggingState? read(String reorderFlexId) {
+  DraggingState? readState(String reorderFlexId) {
     return groupDragStates[reorderFlexId];
   }
 
   @override
-  void write(String reorderFlexId, DraggingState state) {
+  void insertState(String reorderFlexId, DraggingState state) {
     Log.trace('$reorderFlexId Write dragging state: $state');
     groupDragStates[reorderFlexId] = state;
   }
 
   @override
-  void remove(String reorderFlexId) {
+  void removeState(String reorderFlexId) {
     groupDragStates.remove(reorderFlexId);
   }
 
   @override
-  void addKey(
+  void insertDragTarget(
     String reorderFlexId,
     String key,
     GlobalObjectKey<State<StatefulWidget>> value,
   ) {
-    Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId];
+    Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
     if (group == null) {
       group = {};
-      groupDragDragTargets[reorderFlexId] = group;
+      groupDragTargetKeys[reorderFlexId] = group;
     }
     group[key] = value;
   }
 
   @override
-  GlobalObjectKey<State<StatefulWidget>>? readKey(
-      String reorderFlexId, String key) {
-    Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId];
+  GlobalObjectKey<State<StatefulWidget>>? getDragTarget(
+    String reorderFlexId,
+    String key,
+  ) {
+    Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
     if (group != null) {
       return group[key];
     } else {
       return null;
     }
   }
+
+  @override
+  void removeDragTarget(String reorderFlexId) {
+    groupDragTargetKeys.remove(reorderFlexId);
+  }
 }
+
+class ReorderFlexActionImpl extends ReorderFlexAction {}

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

@@ -90,21 +90,21 @@ class AppFlowyBoardGroup extends StatefulWidget {
 
   final DraggingStateStorage? dragStateStorage;
 
-  final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
+  final ReorderDragTargeKeys? dragTargetKeys;
 
-  final GlobalObjectKey reorderFlexKey;
+  final ReorderFlexAction? reorderFlexAction;
 
   const AppFlowyBoardGroup({
     Key? key,
-    required this.reorderFlexKey,
     this.headerBuilder,
     this.footerBuilder,
     required this.cardBuilder,
     required this.onReorder,
     required this.dataSource,
     required this.phantomController,
+    this.reorderFlexAction,
     this.dragStateStorage,
-    this.dragTargetIndexKeyStorage,
+    this.dragTargetKeys,
     this.scrollController,
     this.onDragStarted,
     this.onDragEnded,
@@ -146,9 +146,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
         );
 
         Widget reorderFlex = ReorderFlex(
-          key: widget.reorderFlexKey,
+          key: ValueKey(widget.groupId),
           dragStateStorage: widget.dragStateStorage,
-          dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage,
+          dragTargetKeys: widget.dragTargetKeys,
           scrollController: widget.scrollController,
           config: widget.config,
           onDragStarted: (index) {
@@ -168,6 +168,7 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
           },
           dataSource: widget.dataSource,
           interceptor: interceptor,
+          reorderFlexAction: widget.reorderFlexAction,
           children: children,
         );
 

+ 12 - 8
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart

@@ -41,7 +41,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
   void updateGroupName(String newName) {
     if (groupData.headerData.groupName != newName) {
       groupData.headerData.groupName = newName;
-      notifyListeners();
+      _notify();
     }
   }
 
@@ -56,7 +56,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
     Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
     final item = groupData._items.removeAt(index);
     if (notify) {
-      notifyListeners();
+      _notify();
     }
     return item;
   }
@@ -81,7 +81,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
         '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
     final item = groupData._items.removeAt(fromIndex);
     groupData._items.insert(toIndex, item);
-    notifyListeners();
+    _notify();
     return true;
   }
 
@@ -102,7 +102,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
         groupData._items.add(item);
       }
 
-      if (notify) notifyListeners();
+      if (notify) _notify();
       return true;
     }
   }
@@ -112,7 +112,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
       return false;
     } else {
       groupData._items.add(item);
-      if (notify) notifyListeners();
+      if (notify) _notify();
       return true;
     }
   }
@@ -135,7 +135,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
           '[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index');
     }
 
-    notifyListeners();
+    _notify();
   }
 
   void replaceOrInsertItem(AppFlowyGroupItem newItem) {
@@ -143,10 +143,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
     if (index != -1) {
       groupData._items.removeAt(index);
       groupData._items.insert(index, newItem);
-      notifyListeners();
+      _notify();
     } else {
       groupData._items.add(newItem);
-      notifyListeners();
+      _notify();
     }
   }
 
@@ -154,6 +154,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
     return groupData._items.indexWhere((element) => element.id == item.id) !=
         -1;
   }
+
+  void _notify() {
+    notifyListeners();
+  }
 }
 
 /// [AppFlowyGroupData] represents the data of each group of the Board.

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

@@ -69,9 +69,9 @@ class FlexDragTargetData extends DragTargetData {
 }
 
 abstract class DraggingStateStorage {
-  void write(String reorderFlexId, DraggingState state);
-  void remove(String reorderFlexId);
-  DraggingState? read(String reorderFlexId);
+  void insertState(String reorderFlexId, DraggingState state);
+  void removeState(String reorderFlexId);
+  DraggingState? readState(String reorderFlexId);
 }
 
 class DraggingState {

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

@@ -73,11 +73,15 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
   final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder;
 
   final AnimationController insertAnimationController;
+
   final AnimationController deleteAnimationController;
 
   final bool useMoveAnimation;
+
   final bool draggable;
 
+  final double draggingOpacity;
+
   const ReorderDragTarget({
     Key? key,
     required this.child,
@@ -94,6 +98,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
     this.onAccept,
     this.onLeave,
     this.draggableTargetBuilder,
+    this.draggingOpacity = 0.3,
   }) : super(key: key);
 
   @override
@@ -164,6 +169,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
           feedback: feedbackBuilder,
           childWhenDragging: IgnorePointerWidget(
             useIntrinsicSize: !widget.useMoveAnimation,
+            opacity: widget.draggingOpacity,
             child: widget.child,
           ),
           onDragStarted: () {
@@ -195,7 +201,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
   }
 
   Widget _buildDraggableFeedback(
-      BuildContext context, BoxConstraints constraints, Widget child) {
+    BuildContext context,
+    BoxConstraints constraints,
+    Widget child,
+  ) {
     return Transform(
       transform: Matrix4.rotationZ(0),
       alignment: FractionalOffset.topLeft,
@@ -205,7 +214,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
         clipBehavior: Clip.hardEdge,
         child: ConstrainedBox(
           constraints: constraints,
-          child: Opacity(opacity: 0.3, child: child),
+          child: Opacity(opacity: widget.draggingOpacity, child: child),
         ),
       ),
     );
@@ -274,8 +283,11 @@ class DragTargetAnimation {
 class IgnorePointerWidget extends StatelessWidget {
   final Widget? child;
   final bool useIntrinsicSize;
+  final double opacity;
+
   const IgnorePointerWidget({
     required this.child,
+    required this.opacity,
     this.useIntrinsicSize = false,
     Key? key,
   }) : super(key: key);
@@ -286,11 +298,10 @@ class IgnorePointerWidget extends StatelessWidget {
         ? child
         : SizedBox(width: 0.0, height: 0.0, child: child);
 
-    final opacity = useIntrinsicSize ? 0.3 : 0.0;
     return IgnorePointer(
       ignoring: true,
       child: Opacity(
-        opacity: opacity,
+        opacity: useIntrinsicSize ? opacity : 0.0,
         child: sizedChild,
       ),
     );
@@ -300,8 +311,10 @@ class IgnorePointerWidget extends StatelessWidget {
 class AbsorbPointerWidget extends StatelessWidget {
   final Widget? child;
   final bool useIntrinsicSize;
+  final double opacity;
   const AbsorbPointerWidget({
     required this.child,
+    required this.opacity,
     this.useIntrinsicSize = false,
     Key? key,
   }) : super(key: key);
@@ -312,10 +325,9 @@ class AbsorbPointerWidget extends StatelessWidget {
         ? child
         : SizedBox(width: 0.0, height: 0.0, child: child);
 
-    final opacity = useIntrinsicSize ? 0.3 : 0.0;
     return AbsorbPointer(
       child: Opacity(
-        opacity: opacity,
+        opacity: useIntrinsicSize ? opacity : 0.0,
         child: sizedChild,
       ),
     );
@@ -494,6 +506,7 @@ class _FakeDragTargetState<T extends DragTargetData>
         sizeFactor: widget.deleteAnimationController,
         axis: Axis.vertical,
         child: AbsorbPointerWidget(
+          opacity: 0.3,
           child: widget.child,
         ),
       );
@@ -503,6 +516,7 @@ class _FakeDragTargetState<T extends DragTargetData>
         axis: Axis.vertical,
         child: AbsorbPointerWidget(
           useIntrinsicSize: true,
+          opacity: 0.3,
           child: widget.child,
         ),
       );

+ 2 - 3
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart

@@ -81,7 +81,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
       delegate.cancel();
     } else {
       // Ignore the event if the dragTarget overlaps with the other column's dragTargets.
-      final columnKeys = columnsState.groupDragDragTargets[dragTargetId];
+      final columnKeys = columnsState.groupDragTargetKeys[dragTargetId];
       if (columnKeys != null) {
         final keys = columnKeys.values.toList();
         if (dragTargetData.isOverlapWithWidgets(keys)) {
@@ -102,8 +102,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
           delegate.dragTargetDidMoveToReorderFlex(
               dragTargetId, dragTargetData, index);
 
-          columnsState
-              .getReorderFlexState(groupId: dragTargetId)
+          columnsState.reorderFlexActionMap[dragTargetId]
               ?.resetDragTargetIndex(index);
         }
       });

+ 75 - 26
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -31,9 +31,32 @@ abstract class ReoderFlexItem {
   String get id;
 }
 
-abstract class ReorderDragTargetIndexKeyStorage {
-  void addKey(String reorderFlexId, String key, GlobalObjectKey value);
-  GlobalObjectKey? readKey(String reorderFlexId, String key);
+/// Cache each dragTarget's key.
+/// For the moment, the key is used to locate the render object that will
+/// be passed in the [ScrollPosition]'s [ensureVisible] function.
+///
+abstract class ReorderDragTargeKeys {
+  void insertDragTarget(
+    String reorderFlexId,
+    String key,
+    GlobalObjectKey value,
+  );
+
+  GlobalObjectKey? getDragTarget(
+    String reorderFlexId,
+    String key,
+  );
+
+  void removeDragTarget(String reorderFlexId);
+}
+
+abstract class ReorderFlexAction {
+  void Function(void Function(BuildContext)?)? _scrollToBottom;
+  void Function(void Function(BuildContext)?) get scrollToBottom =>
+      _scrollToBottom!;
+
+  void Function(int)? _resetDragTargetIndex;
+  void Function(int) get resetDragTargetIndex => _resetDragTargetIndex!;
 }
 
 class ReorderFlexConfig {
@@ -78,9 +101,12 @@ class ReorderFlex extends StatefulWidget {
 
   final DragTargetInterceptor? interceptor;
 
+  /// Save the [DraggingState] if the current [ReorderFlex] get reinitialize.
   final DraggingStateStorage? dragStateStorage;
 
-  final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
+  final ReorderDragTargeKeys? dragTargetKeys;
+
+  final ReorderFlexAction? reorderFlexAction;
 
   final bool reorderable;
 
@@ -93,10 +119,11 @@ class ReorderFlex extends StatefulWidget {
     required this.onReorder,
     this.reorderable = true,
     this.dragStateStorage,
-    this.dragTargetIndexKeyStorage,
+    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.'),
@@ -109,7 +136,7 @@ class ReorderFlex extends StatefulWidget {
 }
 
 class ReorderFlexState extends State<ReorderFlex>
-    with ReorderFlexMinxi, TickerProviderStateMixin<ReorderFlex> {
+    with ReorderFlexMixin, TickerProviderStateMixin<ReorderFlex> {
   /// Controls scrolls and measures scroll progress.
   late ScrollController _scrollController;
 
@@ -131,11 +158,11 @@ class ReorderFlexState extends State<ReorderFlex>
   void initState() {
     _notifier = ReorderFlexNotifier();
     final flexId = widget.reorderFlexId;
-    dragState = widget.dragStateStorage?.read(flexId) ??
+    dragState = widget.dragStateStorage?.readState(flexId) ??
         DraggingState(widget.reorderFlexId);
     Log.trace('[DragTarget] init dragState: $dragState');
 
-    widget.dragStateStorage?.remove(flexId);
+    widget.dragStateStorage?.removeState(flexId);
 
     _animation = DragTargetAnimation(
       reorderAnimationDuration: widget.config.reorderAnimationDuration,
@@ -148,6 +175,14 @@ class ReorderFlexState extends State<ReorderFlex>
       vsync: this,
     );
 
+    widget.reorderFlexAction?._scrollToBottom = (fn) {
+      scrollToBottom(fn);
+    };
+
+    widget.reorderFlexAction?._resetDragTargetIndex = (index) {
+      resetDragTargetIndex(index);
+    };
+
     super.initState();
   }
 
@@ -184,7 +219,7 @@ class ReorderFlexState extends State<ReorderFlex>
 
       final indexKey = GlobalObjectKey(child.key!);
       // Save the index key for quick access
-      widget.dragTargetIndexKeyStorage?.addKey(
+      widget.dragTargetKeys?.insertDragTarget(
         widget.reorderFlexId,
         item.id,
         indexKey,
@@ -236,8 +271,12 @@ class ReorderFlexState extends State<ReorderFlex>
   /// [childIndex]: the index of the child in a list
   Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
     return Builder(builder: (context) {
-      final ReorderDragTarget dragTarget =
-          _buildDragTarget(context, child, childIndex, indexKey);
+      final ReorderDragTarget dragTarget = _buildDragTarget(
+        context,
+        child,
+        childIndex,
+        indexKey,
+      );
       int shiftedIndex = childIndex;
 
       if (dragState.isOverlapWithPhantom()) {
@@ -342,6 +381,15 @@ class ReorderFlexState extends State<ReorderFlex>
     });
   }
 
+  static ReorderFlexState of(BuildContext context) {
+    if (context is StatefulElement && context.state is ReorderFlexState) {
+      return context.state as ReorderFlexState;
+    }
+    final ReorderFlexState? result =
+        context.findAncestorStateOfType<ReorderFlexState>();
+    return result!;
+  }
+
   ReorderDragTarget _buildDragTarget(
     BuildContext builderContext,
     Widget child,
@@ -364,7 +412,7 @@ class ReorderFlexState extends State<ReorderFlex>
             "[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
         _startDragging(draggingWidget, draggingIndex, size);
         widget.onDragStarted?.call(draggingIndex);
-        widget.dragStateStorage?.remove(widget.reorderFlexId);
+        widget.dragStateStorage?.removeState(widget.reorderFlexId);
       },
       onDragMoved: (dragTargetData, offset) {
         dragTargetData.dragTargetOffset = offset;
@@ -435,6 +483,7 @@ class ReorderFlexState extends State<ReorderFlex>
       draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
       useMoveAnimation: widget.config.useMoveAnimation,
       draggable: widget.reorderable,
+      draggingOpacity: widget.config.draggingWidgetOpacity,
       child: child,
     );
   }
@@ -487,7 +536,7 @@ class ReorderFlexState extends State<ReorderFlex>
     }
 
     dragState.setStartDraggingIndex(dragTargetIndex);
-    widget.dragStateStorage?.write(
+    widget.dragStateStorage?.insertState(
       widget.reorderFlexId,
       dragState,
     );
@@ -581,46 +630,46 @@ class ReorderFlexState extends State<ReorderFlex>
     }
   }
 
-  void scrollToBottom(VoidCallback? completed) {
+  void scrollToBottom(void Function(BuildContext)? completed) {
     if (_scrolling) {
-      completed?.call();
+      completed?.call(context);
       return;
     }
 
     if (widget.dataSource.items.isNotEmpty) {
       final item = widget.dataSource.items.last;
-      final indexKey = widget.dragTargetIndexKeyStorage?.readKey(
+      final dragTargetKey = widget.dragTargetKeys?.getDragTarget(
         widget.reorderFlexId,
         item.id,
       );
-      if (indexKey == null) {
-        completed?.call();
+      if (dragTargetKey == null) {
+        completed?.call(context);
         return;
       }
 
-      final indexContext = indexKey.currentContext;
-      if (indexContext == null || _scrollController.hasClients == false) {
-        completed?.call();
+      final dragTargetContext = dragTargetKey.currentContext;
+      if (dragTargetContext == null || _scrollController.hasClients == false) {
+        completed?.call(context);
         return;
       }
 
-      final renderObject = indexContext.findRenderObject();
-      if (renderObject != null) {
+      final dragTargetRenderObject = dragTargetContext.findRenderObject();
+      if (dragTargetRenderObject != null) {
         _scrolling = true;
         _scrollController.position
             .ensureVisible(
-          renderObject,
+          dragTargetRenderObject,
           alignment: 0.5,
           duration: const Duration(milliseconds: 120),
         )
             .then((value) {
           setState(() {
             _scrolling = false;
-            completed?.call();
+            completed?.call(context);
           });
         });
       } else {
-        completed?.call();
+        completed?.call(context);
       }
     }
   }

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart

@@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
 import '../transitions.dart';
 import 'drag_target.dart';
 
-mixin ReorderFlexMinxi {
+mixin ReorderFlexMixin {
   @protected
   Widget makeAppearingWidget(
     Widget child,

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/pubspec.yaml

@@ -1,6 +1,6 @@
 name: appflowy_board
 description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. 
-version: 0.0.7
+version: 0.0.8
 homepage: https://github.com/AppFlowy-IO/AppFlowy
 repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board
 

+ 4 - 4
frontend/rust-lib/Cargo.lock

@@ -1444,9 +1444,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
 
 [[package]]
 name = "hashbrown"
-version = "0.11.2"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
 name = "heck"
@@ -1610,9 +1610,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
 
 [[package]]
 name = "indexmap"
-version = "1.8.1"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
 dependencies = [
  "autocfg",
  "hashbrown",

+ 1 - 1
frontend/rust-lib/flowy-grid/Cargo.toml

@@ -34,7 +34,7 @@ rayon = "1.5.2"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = {version = "1.0"}
 serde_repr = "0.1"
-indexmap = {version = "1.8.1", features = ["serde"]}
+indexmap = {version = "1.9.1", features = ["serde"]}
 fancy-regex = "0.10.0"
 regex = "1.5.6"
 url = { version = "2"}

+ 1 - 0
frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs

@@ -98,6 +98,7 @@ pub struct MoveGroupPayloadPB {
     pub to_group_id: String,
 }
 
+#[derive(Debug)]
 pub struct MoveGroupParams {
     pub view_id: String,
     pub from_group_id: String,

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

@@ -368,6 +368,7 @@ impl GridRevisionEditor {
         Ok(row_pb)
     }
 
+    #[tracing::instrument(level = "trace", skip_all, err)]
     pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let _ = self.view_manager.move_group(params).await?;
         Ok(())

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

@@ -173,6 +173,7 @@ impl GridViewRevisionEditor {
         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<()> {
         let _ = self
             .group_controller
@@ -180,7 +181,7 @@ impl GridViewRevisionEditor {
             .await
             .move_group(&params.from_group_id, &params.to_group_id)?;
         match self.group_controller.read().await.get_group(&params.from_group_id) {
-            None => {}
+            None => tracing::warn!("Can not find the group with id: {}", params.from_group_id),
             Some((index, group)) => {
                 let inserted_group = InsertedGroupPB {
                     group: GroupPB::from(group),
@@ -228,7 +229,11 @@ impl GridViewRevisionEditor {
             let _ = self
                 .modify(|pad| {
                     let configuration = default_group_configuration(&field_rev);
-                    let changeset = pad.insert_group(&params.field_id, &params.field_type_rev, configuration)?;
+                    let changeset = pad.insert_or_update_group_configuration(
+                        &params.field_id,
+                        &params.field_type_rev,
+                        configuration,
+                    )?;
                     Ok(changeset)
                 })
                 .await?;
@@ -496,10 +501,11 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
         let field_id = field_id.to_owned();
 
         wrap_future(async move {
-            let changeset = view_pad
-                .write()
-                .await
-                .insert_group(&field_id, &field_type, group_configuration)?;
+            let changeset = view_pad.write().await.insert_or_update_group_configuration(
+                &field_id,
+                &field_type,
+                group_configuration,
+            )?;
 
             if let Some(changeset) = changeset {
                 let _ = apply_change(&user_id, rev_manager, changeset).await?;

+ 45 - 62
frontend/rust-lib/flowy-grid/src/services/group/configuration.rs

@@ -1,5 +1,5 @@
 use crate::entities::{GroupPB, GroupViewChangesetPB};
-use crate::services::group::{default_group_configuration, GeneratedGroup, Group};
+use crate::services::group::{default_group_configuration, make_default_group, GeneratedGroup, Group};
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::{
     FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision,
@@ -29,10 +29,7 @@ impl<T> std::fmt::Display for GroupContext<T> {
         self.groups_map.iter().for_each(|(_, group)| {
             let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len()));
         });
-        let _ = f.write_fmt(format_args!(
-            "Default group has {} rows \n",
-            self.default_group.rows.len()
-        ));
+
         Ok(())
     }
 }
@@ -44,7 +41,7 @@ pub struct GroupContext<C> {
     field_rev: Arc<FieldRevision>,
     groups_map: IndexMap<String, Group>,
     /// default_group is used to store the rows that don't belong to any groups.
-    default_group: Group,
+    // default_group: Group,
     writer: Arc<dyn GroupConfigurationWriter>,
 }
 
@@ -59,16 +56,6 @@ where
         reader: Arc<dyn GroupConfigurationReader>,
         writer: Arc<dyn GroupConfigurationWriter>,
     ) -> FlowyResult<Self> {
-        let default_group_id = format!("{}_default_group", view_id);
-        let default_group = Group {
-            id: default_group_id,
-            field_id: field_rev.id.clone(),
-            name: format!("No {}", field_rev.name),
-            is_default: true,
-            is_visible: true,
-            rows: vec![],
-            filter_content: "".to_string(),
-        };
         let configuration = match reader.get_configuration().await {
             None => {
                 let default_configuration = default_group_configuration(&field_rev);
@@ -80,24 +67,22 @@ where
             Some(configuration) => configuration,
         };
 
-        // let configuration = C::from_configuration_content(&configuration_rev.content)?;
         Ok(Self {
             view_id,
             field_rev,
             groups_map: IndexMap::new(),
-            default_group,
             writer,
             configuration,
             configuration_content: PhantomData,
         })
     }
 
-    pub(crate) fn get_default_group(&self) -> &Group {
-        &self.default_group
+    pub(crate) fn get_default_group(&self) -> Option<&Group> {
+        self.groups_map.get(&self.field_rev.id)
     }
 
-    pub(crate) fn get_mut_default_group(&mut self) -> &mut Group {
-        &mut self.default_group
+    pub(crate) fn get_mut_default_group(&mut self) -> Option<&mut Group> {
+        self.groups_map.get_mut(&self.field_rev.id)
     }
 
     /// Returns the groups without the default group
@@ -122,8 +107,6 @@ where
         self.groups_map.iter_mut().for_each(|(_, group)| {
             each(group);
         });
-
-        each(&mut self.default_group);
     }
 
     pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> {
@@ -131,18 +114,23 @@ where
         let to_index = self.groups_map.get_index_of(to_id);
         match (from_index, to_index) {
             (Some(from_index), Some(to_index)) => {
-                self.groups_map.swap_indices(from_index, to_index);
+                self.groups_map.move_index(from_index, to_index);
+
                 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);
-                    if let (Some(from), Some(to)) = (from_index, to_index) {
-                        configuration.groups.swap(from, to);
+                    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);
                     }
-                    true
+
+                    from_index.is_some() && to_index.is_some()
                 })?;
                 Ok(())
             }
-            _ => Err(FlowyError::out_of_bounds()),
+            _ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")),
         }
     }
 
@@ -150,7 +138,6 @@ where
     pub(crate) fn init_groups(
         &mut self,
         generated_groups: Vec<GeneratedGroup>,
-        reset: bool,
     ) -> FlowyResult<Option<GroupViewChangesetPB>> {
         let mut new_groups = vec![];
         let mut filter_content_map = HashMap::new();
@@ -159,16 +146,17 @@ where
             new_groups.push(generate_group.group_rev);
         });
 
+        let mut old_groups = self.configuration.groups.clone();
+        if !old_groups.iter().any(|group| group.id == self.field_rev.id) {
+            old_groups.push(make_default_group(&self.field_rev));
+        }
+
         let MergeGroupResult {
             mut all_group_revs,
             new_group_revs,
             updated_group_revs: _,
             deleted_group_revs,
-        } = if reset {
-            merge_groups(&[], new_groups)
-        } else {
-            merge_groups(&self.configuration.groups, new_groups)
-        };
+        } = merge_groups(old_groups, new_groups);
 
         let deleted_group_ids = deleted_group_revs
             .into_iter()
@@ -197,31 +185,23 @@ where
                     Some(pos) => {
                         let mut old_group = configuration.groups.remove(pos);
                         group_rev.update_with_other(&old_group);
+                        is_changed = is_group_changed(group_rev, &old_group);
 
-                        // Take the GroupRevision if the name has changed
-                        if is_group_changed(group_rev, &old_group) {
-                            old_group.name = group_rev.name.clone();
-                            is_changed = true;
-                            configuration.groups.insert(pos, old_group);
-                        }
+                        old_group.name = group_rev.name.clone();
+                        configuration.groups.insert(pos, old_group);
                     }
                 }
             }
             is_changed
         })?;
 
-        // The len of the filter_content_map should equal to the len of the all_group_revs
-        debug_assert_eq!(filter_content_map.len(), all_group_revs.len());
         all_group_revs.into_iter().for_each(|group_rev| {
-            if let Some(filter_content) = filter_content_map.get(&group_rev.id) {
-                let group = Group::new(
-                    group_rev.id,
-                    self.field_rev.id.clone(),
-                    group_rev.name,
-                    filter_content.clone(),
-                );
-                self.groups_map.insert(group.id.clone(), group);
-            }
+            let filter_content = filter_content_map
+                .get(&group_rev.id)
+                .cloned()
+                .unwrap_or_else(|| "".to_owned());
+            let group = Group::new(group_rev.id, self.field_rev.id.clone(), group_rev.name, filter_content);
+            self.groups_map.insert(group.id.clone(), group);
         });
 
         let new_groups = new_group_revs
@@ -269,6 +249,7 @@ where
         Ok(())
     }
 
+    #[tracing::instrument(level = "trace", skip_all, err)]
     pub fn save_configuration(&self) -> FlowyResult<()> {
         let configuration = (&*self.configuration).clone();
         let writer = self.writer.clone();
@@ -311,13 +292,14 @@ where
     }
 }
 
-fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) -> MergeGroupResult {
+fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>) -> MergeGroupResult {
     let mut merge_result = MergeGroupResult::new();
-    if old_groups.is_empty() {
-        merge_result.all_group_revs = new_groups.clone();
-        merge_result.new_group_revs = new_groups;
-        return merge_result;
-    }
+    // if old_groups.is_empty() {
+    //     merge_result.all_group_revs.extend(new_groups.clone());
+    //     merge_result.all_group_revs.push(default_group);
+    //     merge_result.new_group_revs = new_groups;
+    //     return merge_result;
+    // }
 
     // group_map is a helper map is used to filter out the new groups.
     let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new();
@@ -329,19 +311,20 @@ fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) ->
     for old in old_groups {
         if let Some(new) = new_group_map.remove(&old.id) {
             merge_result.all_group_revs.push(new.clone());
-            if is_group_changed(&new, old) {
+            if is_group_changed(&new, &old) {
                 merge_result.updated_group_revs.push(new);
             }
         } else {
-            merge_result.deleted_group_revs.push(old.clone());
+            merge_result.all_group_revs.push(old);
         }
     }
 
     // Find out the new groups
+    new_group_map.reverse();
     let new_groups = new_group_map.into_values();
     for (_, group) in new_groups.into_iter().enumerate() {
-        merge_result.all_group_revs.push(group.clone());
-        merge_result.new_group_revs.push(group);
+        merge_result.all_group_revs.insert(0, group.clone());
+        merge_result.new_group_revs.insert(0, group);
     }
     merge_result
 }

+ 26 - 20
frontend/rust-lib/flowy-grid/src/services/group/controller.rs

@@ -88,7 +88,7 @@ where
     pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
         let type_option = field_rev.get_type_option::<T>(field_rev.ty);
         let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
-        let _ = configuration.init_groups(groups, true)?;
+        let _ = configuration.init_groups(groups)?;
 
         Ok(Self {
             field_id: field_rev.id.clone(),
@@ -105,8 +105,8 @@ where
         &mut self,
         row_rev: &RowRevision,
         other_group_changesets: &[GroupChangesetPB],
-    ) -> GroupChangesetPB {
-        let default_group = self.group_ctx.get_mut_default_group();
+    ) -> Option<GroupChangesetPB> {
+        let default_group = self.group_ctx.get_mut_default_group()?;
 
         // [other_group_inserted_row] contains all the inserted rows except the default group.
         let other_group_inserted_row = other_group_changesets
@@ -163,7 +163,7 @@ where
         }
         default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id));
         changeset.deleted_rows.extend(deleted_row_ids);
-        changeset
+        Some(changeset)
     }
 }
 
@@ -182,11 +182,14 @@ where
 
     fn groups(&self) -> Vec<Group> {
         if self.use_default_group() {
-            let mut groups: Vec<Group> = self.group_ctx.groups().into_iter().cloned().collect();
-            groups.push(self.group_ctx.get_default_group().clone());
-            groups
-        } else {
             self.group_ctx.groups().into_iter().cloned().collect()
+        } else {
+            self.group_ctx
+                .groups()
+                .into_iter()
+                .filter(|group| group.id != self.field_id)
+                .cloned()
+                .collect::<Vec<_>>()
         }
     }
 
@@ -216,17 +219,18 @@ where
                     }
                 }
 
-                if grouped_rows.is_empty() {
-                    self.group_ctx.get_mut_default_group().add_row(row_rev.into());
-                } else {
+                if !grouped_rows.is_empty() {
                     for group_row in grouped_rows {
                         if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) {
                             group.add_row(group_row.row);
                         }
                     }
+                    continue;
                 }
-            } else {
-                self.group_ctx.get_mut_default_group().add_row(row_rev.into());
+            }
+            match self.group_ctx.get_mut_default_group() {
+                None => {}
+                Some(default_group) => default_group.add_row(row_rev.into()),
             }
         }
 
@@ -247,10 +251,11 @@ where
             let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1;
             let cell_data = cell_bytes.parser::<P>()?;
             let mut changesets = self.add_row_if_match(row_rev, &cell_data);
-            let default_group_changeset = self.update_default_group(row_rev, &changesets);
-            tracing::trace!("default_group_changeset: {}", default_group_changeset);
-            if !default_group_changeset.is_empty() {
-                changesets.push(default_group_changeset);
+            if let Some(default_group_changeset) = self.update_default_group(row_rev, &changesets) {
+                tracing::trace!("default_group_changeset: {}", default_group_changeset);
+                if !default_group_changeset.is_empty() {
+                    changesets.push(default_group_changeset);
+                }
             }
             Ok(changesets)
         } else {
@@ -268,12 +273,13 @@ where
             let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1;
             let cell_data = cell_bytes.parser::<P>()?;
             Ok(self.remove_row_if_match(row_rev, &cell_data))
-        } else {
-            let group = self.group_ctx.get_default_group();
+        } else if let Some(group) = self.group_ctx.get_default_group() {
             Ok(vec![GroupChangesetPB::delete(
                 group.id.clone(),
                 vec![row_rev.id.clone()],
             )])
+        } else {
+            Ok(vec![])
         }
     }
 
@@ -297,7 +303,7 @@ where
     fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
         let type_option = field_rev.get_type_option::<T>(field_rev.ty);
         let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option);
-        let changeset = self.group_ctx.init_groups(groups, false)?;
+        let changeset = self.group_ctx.init_groups(groups)?;
         Ok(changeset)
     }
 }

+ 3 - 2
frontend/rust-lib/flowy-grid/src/services/group/entities.rs

@@ -9,16 +9,17 @@ pub struct Group {
     pub is_visible: bool,
     pub(crate) rows: Vec<RowPB>,
 
-    /// [content] is used to determine which group the cell belongs to.
+    /// [filter_content] is used to determine which group the cell belongs to.
     pub filter_content: String,
 }
 
 impl Group {
     pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self {
+        let is_default = id == field_id;
         Self {
             id,
             field_id,
-            is_default: false,
+            is_default,
             is_visible: true,
             name,
             rows: vec![],

+ 21 - 3
frontend/rust-lib/flowy-grid/src/services/group/group_util.rs

@@ -8,8 +8,8 @@ use crate::services::group::{
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
     CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
-    LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
-    TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
+    GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision,
+    SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
 };
 use std::sync::Arc;
 
@@ -79,7 +79,7 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
     let field_id = field_rev.id.clone();
     let field_type_rev = field_rev.ty;
     let field_type: FieldType = field_rev.ty.into();
-    match field_type {
+    let mut group_configuration_rev = match field_type {
         FieldType::RichText => {
             GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default())
                 .unwrap()
@@ -112,5 +112,23 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
         FieldType::URL => {
             GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap()
         }
+    };
+
+    // Append the no `status` group
+    let default_group_rev = GroupRevision {
+        id: field_rev.id.clone(),
+        name: format!("No {}", field_rev.name),
+        visible: true,
+    };
+
+    group_configuration_rev.groups.push(default_group_rev);
+    group_configuration_rev
+}
+
+pub fn make_default_group(field_rev: &FieldRevision) -> GroupRevision {
+    GroupRevision {
+        id: field_rev.id.clone(),
+        name: format!("No {}", field_rev.name),
+        visible: true,
     }
 }

+ 23 - 1
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -370,6 +370,28 @@ async fn group_move_group_test() {
     test.run_scripts(scripts).await;
 }
 
+#[tokio::test]
+async fn group_default_move_group_test() {
+    let mut test = GridGroupTest::new().await;
+    let group_0 = test.group_at_index(0).await;
+    let group_3 = test.group_at_index(3).await;
+    let scripts = vec![
+        MoveGroup {
+            from_group_index: 3,
+            to_group_index: 0,
+        },
+        AssertGroup {
+            group_index: 0,
+            expected_group: group_3,
+        },
+        AssertGroup {
+            group_index: 1,
+            expected_group: group_0,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
 #[tokio::test]
 async fn group_insert_single_select_option_test() {
     let mut test = GridGroupTest::new().await;
@@ -402,7 +424,7 @@ async fn group_group_by_other_field() {
             group_index: 1,
             row_count: 2,
         },
-        AssertGroupCount(4),
+        AssertGroupCount(5),
     ];
     test.run_scripts(scripts).await;
 }

+ 4 - 4
shared-lib/Cargo.lock

@@ -650,9 +650,9 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.11.2"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
 name = "heck"
@@ -732,9 +732,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.8.1"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
 dependencies = [
  "autocfg",
  "hashbrown",

+ 1 - 1
shared-lib/flowy-grid-data-model/Cargo.toml

@@ -12,7 +12,7 @@ serde_json = {version = "1.0"}
 serde_repr = "0.1"
 nanoid = "0.4.0"
 flowy-error-code = { path = "../flowy-error-code"}
-indexmap = {version = "1.8.1", features = ["serde"]}
+indexmap = {version = "1.9.1", features = ["serde"]}
 tracing = { version = "0.1", features = ["log"] }
 
 [build-dependencies]

+ 0 - 8
shared-lib/flowy-grid-data-model/src/revision/group_rev.rs

@@ -128,14 +128,6 @@ impl GroupRevision {
         }
     }
 
-    pub fn default_group(id: String, group_name: String) -> Self {
-        Self {
-            id,
-            name: group_name,
-            visible: true,
-        }
-    }
-
     pub fn update_with_other(&mut self, other: &GroupRevision) {
         self.visible = other.visible
     }

+ 1 - 1
shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs

@@ -66,7 +66,7 @@ impl GridViewRevisionPad {
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    pub fn insert_group(
+    pub fn insert_or_update_group_configuration(
         &mut self,
         field_id: &str,
         field_type: &FieldTypeRevision,