Parcourir la source

Merge pull request #920 from AppFlowy-IO/feat/edit_create_card

Feat/edit create card
Nathan.fooo il y a 2 ans
Parent
commit
fcb144e514
20 fichiers modifiés avec 617 ajouts et 230 suppressions
  1. 79 37
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  2. 1 1
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  3. 41 32
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  4. 29 12
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  5. 72 28
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  6. 3 2
      frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart
  7. 16 4
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  8. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  9. 2 2
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  10. 119 19
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  11. 15 13
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
  12. 0 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  13. 58 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart
  14. 17 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  15. 19 18
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart
  16. 101 12
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  17. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
  18. 25 26
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  19. 5 6
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  20. 13 9
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

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

@@ -72,28 +72,33 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           createRow: (groupId) async {
             final result = await _gridDataController.createBoardCard(groupId);
             result.fold(
-              (rowPB) {
-                emit(state.copyWith(editingRow: some(rowPB)));
-              },
+              (_) {},
               (err) => Log.error(err),
             );
           },
+          didCreateRow: (String groupId, RowPB row) {
+            emit(state.copyWith(
+              editingRow: Some(BoardEditingRow(columnId: groupId, row: row)),
+            ));
+          },
           endEditRow: (rowId) {
             assert(state.editingRow.isSome());
-            state.editingRow.fold(() => null, (row) {
-              assert(row.id == rowId);
+            state.editingRow.fold(() => null, (editingRow) {
+              assert(editingRow.row.id == rowId);
               emit(state.copyWith(editingRow: none()));
             });
           },
           didReceiveGridUpdate: (GridPB grid) {
             emit(state.copyWith(grid: Some(grid)));
           },
-          didReceiveRows: (List<RowInfo> rowInfos) {
-            emit(state.copyWith(rowInfos: rowInfos));
-          },
           didReceiveError: (FlowyError error) {
             emit(state.copyWith(noneOrError: some(error)));
           },
+          didReceiveGroups: (List<GroupPB> groups) {
+            emit(state.copyWith(
+              groupIds: groups.map((group) => group.groupId).toList(),
+            ));
+          },
         );
       },
     );
@@ -135,7 +140,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   void initializeGroups(List<GroupPB> groups) {
     for (final group in groups) {
-      final delegate = GroupControllerDelegateImpl(boardController);
+      final delegate = GroupControllerDelegateImpl(
+        controller: boardController,
+        didAddColumnItem: (groupId, row) {
+          add(BoardEvent.didCreateRow(groupId, row));
+        },
+      );
       final controller = GroupController(
         gridId: state.gridId,
         group: group,
@@ -163,16 +173,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           return AFBoardColumnData(
             id: group.groupId,
             name: group.desc,
-            items: _buildRows(group.rows),
+            items: _buildRows(group),
             customData: group,
           );
         }).toList();
 
         boardController.addColumns(columns);
         initializeGroups(groups);
-      },
-      onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
-        add(BoardEvent.didReceiveRows(rowInfos));
+        add(BoardEvent.didReceiveGroups(groups));
       },
       onDeletedGroup: (groupIds) {
         //
@@ -196,9 +204,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  List<AFColumnItem> _buildRows(List<RowPB> rows) {
-    final items = rows.map((row) {
-      return BoardColumnItem(row: row);
+  List<AFColumnItem> _buildRows(GroupPB group) {
+    final items = group.rows.map((row) {
+      return BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+      );
     }).toList();
 
     return <AFColumnItem>[...items];
@@ -219,15 +230,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
 @freezed
 class BoardEvent with _$BoardEvent {
-  const factory BoardEvent.initial() = InitialGrid;
+  const factory BoardEvent.initial() = _InitialBoard;
   const factory BoardEvent.createRow(String groupId) = _CreateRow;
+  const factory BoardEvent.didCreateRow(String groupId, RowPB row) =
+      _DidCreateRow;
   const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
   const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
-  const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
-      _DidReceiveRows;
   const factory BoardEvent.didReceiveGridUpdate(
     GridPB grid,
   ) = _DidReceiveGridUpdate;
+  const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
+      _DidReceiveGroups;
 }
 
 @freezed
@@ -235,16 +248,16 @@ class BoardState with _$BoardState {
   const factory BoardState({
     required String gridId,
     required Option<GridPB> grid,
-    required Option<RowPB> editingRow,
-    required List<RowInfo> rowInfos,
+    required List<String> groupIds,
+    required Option<BoardEditingRow> editingRow,
     required GridLoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _BoardState;
 
   factory BoardState.initial(String gridId) => BoardState(
-        rowInfos: [],
         grid: none(),
         gridId: gridId,
+        groupIds: [],
         editingRow: none(),
         noneOrError: none(),
         loadingState: const _Loading(),
@@ -284,39 +297,68 @@ class GridFieldEquatable extends Equatable {
 class BoardColumnItem extends AFColumnItem {
   final RowPB row;
 
-  BoardColumnItem({required this.row});
+  final String fieldId;
 
-  @override
-  String get id => row.id;
-}
+  final bool requestFocus;
+
+  BoardColumnItem({
+    required this.row,
+    required this.fieldId,
+    this.requestFocus = false,
+  });
 
-class CreateCardItem extends AFColumnItem {
   @override
-  String get id => '$CreateCardItem';
+  String get id => row.id;
 }
 
 class GroupControllerDelegateImpl extends GroupControllerDelegate {
   final AFBoardDataController controller;
+  final void Function(String, RowPB) didAddColumnItem;
 
-  GroupControllerDelegateImpl(this.controller);
+  GroupControllerDelegateImpl({
+    required this.controller,
+    required this.didAddColumnItem,
+  });
 
   @override
-  void insertRow(String groupId, RowPB row, int? index) {
-    final item = BoardColumnItem(row: row);
+  void insertRow(GroupPB group, RowPB row, int? index) {
     if (index != null) {
-      controller.insertColumnItem(groupId, index, item);
+      final item = BoardColumnItem(row: row, fieldId: group.fieldId);
+      controller.insertColumnItem(group.groupId, index, item);
     } else {
-      controller.addColumnItem(groupId, item);
+      final item = BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+        requestFocus: true,
+      );
+      controller.addColumnItem(group.groupId, item);
+      didAddColumnItem(group.groupId, row);
     }
   }
 
   @override
-  void removeRow(String groupId, String rowId) {
-    controller.removeColumnItem(groupId, rowId);
+  void removeRow(GroupPB group, String rowId) {
+    controller.removeColumnItem(group.groupId, rowId);
   }
 
   @override
-  void updateRow(String groupId, RowPB row) {
-    controller.updateColumnItem(groupId, BoardColumnItem(row: row));
+  void updateRow(GroupPB group, RowPB row) {
+    controller.updateColumnItem(
+      group.groupId,
+      BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+      ),
+    );
   }
 }
+
+class BoardEditingRow {
+  String columnId;
+  RowPB row;
+
+  BoardEditingRow({
+    required this.columnId,
+    required this.row,
+  });
+}

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

@@ -60,7 +60,7 @@ class BoardDataController {
     required OnGridChanged onGridChanged,
     OnFieldsChanged? onFieldsChanged,
     required DidLoadGroups didLoadGroups,
-    required OnRowsChanged onRowsChanged,
+    OnRowsChanged? onRowsChanged,
     required OnUpdatedGroup onUpdatedGroup,
     required OnDeletedGroup onDeletedGroup,
     required OnInsertedGroup onInsertedGroup,

+ 41 - 32
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -14,10 +13,12 @@ import 'card_data_controller.dart';
 part 'card_bloc.freezed.dart';
 
 class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+  final String fieldId;
   final RowFFIService _rowService;
   final CardDataController _dataController;
 
   BoardCardBloc({
+    required this.fieldId,
     required String gridId,
     required CardDataController dataController,
   })  : _rowService = RowFFIService(
@@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
           blockId: dataController.rowPB.blockId,
         ),
         _dataController = dataController,
-        super(BoardCardState.initial(
-            dataController.rowPB, dataController.loadData())) {
+        super(
+          BoardCardState.initial(
+            dataController.rowPB,
+            _makeCells(fieldId, dataController.loadData()),
+          ),
+        ) {
     on<BoardCardEvent>(
       (event, emit) async {
-        await event.map(
-          initial: (_InitialRow value) async {
+        await event.when(
+          initial: () async {
             await _startListening();
           },
-          didReceiveCells: (_DidReceiveCells value) async {
-            final cells = value.gridCellMap.values
-                .map((e) => GridCellEquatable(e.field))
-                .toList();
+          didReceiveCells: (cells, reason) async {
             emit(state.copyWith(
-              gridCellMap: value.gridCellMap,
-              cells: UnmodifiableListView(cells),
-              changeReason: value.reason,
+              cells: cells,
+              changeReason: reason,
             ));
           },
         );
@@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
     return RowInfo(
       gridId: _rowService.gridId,
       fields: UnmodifiableListView(
-        state.cells.map((cell) => cell._field).toList(),
+        state.cells.map((cell) => cell.identifier.field).toList(),
       ),
       rowPB: state.rowPB,
     );
@@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
 
   Future<void> _startListening() async {
     _dataController.addListener(
-      onRowChanged: (cells, reason) {
+      onRowChanged: (cellMap, reason) {
         if (!isClosed) {
+          final cells = _makeCells(fieldId, cellMap);
           add(BoardCardEvent.didReceiveCells(cells, reason));
         }
       },
@@ -75,42 +77,49 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
   }
 }
 
+UnmodifiableListView<BoardCellEquatable> _makeCells(
+    String fieldId, GridCellMap originalCellMap) {
+  List<BoardCellEquatable> cells = [];
+  for (final entry in originalCellMap.entries) {
+    if (entry.value.fieldId != fieldId) {
+      cells.add(BoardCellEquatable(entry.value));
+    }
+  }
+  return UnmodifiableListView(cells);
+}
+
 @freezed
 class BoardCardEvent with _$BoardCardEvent {
   const factory BoardCardEvent.initial() = _InitialRow;
   const factory BoardCardEvent.didReceiveCells(
-      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
+    UnmodifiableListView<BoardCellEquatable> cells,
+    RowsChangedReason reason,
+  ) = _DidReceiveCells;
 }
 
 @freezed
 class BoardCardState with _$BoardCardState {
   const factory BoardCardState({
     required RowPB rowPB,
-    required GridCellMap gridCellMap,
-    required UnmodifiableListView<GridCellEquatable> cells,
+    required UnmodifiableListView<BoardCellEquatable> cells,
     RowsChangedReason? changeReason,
   }) = _BoardCardState;
 
-  factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
-      BoardCardState(
-        rowPB: rowPB,
-        gridCellMap: cellDataMap,
-        cells: UnmodifiableListView(
-          cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
-        ),
-      );
+  factory BoardCardState.initial(
+          RowPB rowPB, UnmodifiableListView<BoardCellEquatable> cells) =>
+      BoardCardState(rowPB: rowPB, cells: cells);
 }
 
-class GridCellEquatable extends Equatable {
-  final FieldPB _field;
+class BoardCellEquatable extends Equatable {
+  final GridCellIdentifier identifier;
 
-  const GridCellEquatable(FieldPB field) : _field = field;
+  const BoardCellEquatable(this.identifier);
 
   @override
   List<Object?> get props => [
-        _field.id,
-        _field.fieldType,
-        _field.visibility,
-        _field.width,
+        identifier.field.id,
+        identifier.field.fieldType,
+        identifier.field.visibility,
+        identifier.field.width,
       ];
 }

+ 29 - 12
frontend/app_flowy/lib/plugins/board/application/group_controller.dart

@@ -1,15 +1,14 @@
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
-
 import 'group_listener.dart';
 
 typedef OnGroupError = void Function(FlowyError);
 
 abstract class GroupControllerDelegate {
-  void removeRow(String groupId, String rowId);
-  void insertRow(String groupId, RowPB row, int? index);
-  void updateRow(String groupId, RowPB row);
+  void removeRow(GroupPB group, String rowId);
+  void insertRow(GroupPB group, RowPB row, int? index);
+  void updateRow(GroupPB group, RowPB row);
 }
 
 class GroupController {
@@ -37,12 +36,11 @@ class GroupController {
         (GroupChangesetPB changeset) {
           for (final deletedRow in changeset.deletedRows) {
             group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
-            delegate.removeRow(group.groupId, deletedRow);
+            delegate.removeRow(group, deletedRow);
           }
 
           for (final insertedRow in changeset.insertedRows) {
             final index = insertedRow.hasIndex() ? insertedRow.index : null;
-
             if (insertedRow.hasIndex() &&
                 group.rows.length > insertedRow.index) {
               group.rows.insert(insertedRow.index, insertedRow.row);
@@ -50,11 +48,7 @@ class GroupController {
               group.rows.add(insertedRow.row);
             }
 
-            delegate.insertRow(
-              group.groupId,
-              insertedRow.row,
-              index,
-            );
+            delegate.insertRow(group, insertedRow.row, index);
           }
 
           for (final updatedRow in changeset.updatedRows) {
@@ -66,7 +60,7 @@ class GroupController {
               group.rows[index] = updatedRow;
             }
 
-            delegate.updateRow(group.groupId, updatedRow);
+            delegate.updateRow(group, updatedRow);
           }
         },
         (err) => Log.error(err),
@@ -74,6 +68,29 @@ class GroupController {
     });
   }
 
+  // GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
+  //   final insertedRows = changeset.insertedRows
+  //       .where(
+  //         (delete) => !changeset.deletedRows.contains(delete.row.id),
+  //       )
+  //       .toList();
+
+  //   final deletedRows = changeset.deletedRows
+  //       .where((deletedRowId) =>
+  //           changeset.insertedRows
+  //               .indexWhere((insert) => insert.row.id == deletedRowId) ==
+  //           -1)
+  //       .toList();
+
+  //   return changeset.rebuild((rebuildChangeset) {
+  //     rebuildChangeset.insertedRows.clear();
+  //     rebuildChangeset.insertedRows.addAll(insertedRows);
+
+  //     rebuildChangeset.deletedRows.clear();
+  //     rebuildChangeset.deletedRows.addAll(deletedRows);
+  //   });
+  // }
+
   Future<void> dispose() async {
     _listener.stop();
   }

+ 72 - 28
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -32,13 +32,15 @@ class BoardPage extends StatelessWidget {
       create: (context) =>
           BoardBloc(view: view)..add(const BoardEvent.initial()),
       child: BlocBuilder<BoardBloc, BoardState>(
+        buildWhen: (previous, current) =>
+            previous.loadingState != current.loadingState,
         builder: (context, state) {
           return state.loadingState.map(
             loading: (_) =>
                 const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) {
               return result.successOrFail.fold(
-                (_) => BoardContent(),
+                (_) => const BoardContent(),
                 (err) => FlowyErrorPage(err.toString()),
               );
             },
@@ -49,42 +51,82 @@ class BoardPage extends StatelessWidget {
   }
 }
 
-class BoardContent extends StatelessWidget {
+class BoardContent extends StatefulWidget {
+  const BoardContent({Key? key}) : super(key: key);
+
+  @override
+  State<BoardContent> createState() => _BoardContentState();
+}
+
+class _BoardContentState extends State<BoardContent> {
+  late ScrollController scrollController;
+  late AFBoardScrollManager scrollManager;
+
   final config = AFBoardConfig(
     columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
   );
 
-  BoardContent({Key? key}) : super(key: key);
+  @override
+  void initState() {
+    scrollController = ScrollController();
+    scrollManager = AFBoardScrollManager();
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<BoardBloc, BoardState>(
-      builder: (context, state) {
-        return Container(
-          color: Colors.white,
-          child: Padding(
-            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
-            child: AFBoard(
-              scrollController: ScrollController(),
-              dataController: context.read<BoardBloc>().boardController,
-              headerBuilder: _buildHeader,
-              footBuilder: _buildFooter,
-              cardBuilder: (_, column, columnItem) => _buildCard(
-                context,
-                column,
-                columnItem,
-              ),
-              columnConstraints: const BoxConstraints.tightFor(width: 240),
-              config: AFBoardConfig(
-                columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
-              ),
-            ),
-          ),
+    return BlocListener<BoardBloc, BoardState>(
+      listener: (context, state) {
+        state.editingRow.fold(
+          () => null,
+          (editingRow) {
+            WidgetsBinding.instance.addPostFrameCallback((_) {
+              scrollManager.scrollToBottom(editingRow.columnId, () {
+                context
+                    .read<BoardBloc>()
+                    .add(BoardEvent.endEditRow(editingRow.row.id));
+              });
+            });
+          },
         );
       },
+      child: BlocBuilder<BoardBloc, BoardState>(
+        buildWhen: (previous, current) =>
+            previous.groupIds.length != current.groupIds.length,
+        builder: (context, state) {
+          return Container(
+            color: Colors.white,
+            child: Padding(
+              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
+              child: AFBoard(
+                scrollManager: scrollManager,
+                scrollController: scrollController,
+                dataController: context.read<BoardBloc>().boardController,
+                headerBuilder: _buildHeader,
+                footBuilder: _buildFooter,
+                cardBuilder: (_, column, columnItem) => _buildCard(
+                  context,
+                  column,
+                  columnItem,
+                ),
+                columnConstraints: const BoxConstraints.tightFor(width: 240),
+                config: AFBoardConfig(
+                  columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+                ),
+              ),
+            ),
+          );
+        },
+      ),
     );
   }
 
+  @override
+  void dispose() {
+    scrollController.dispose();
+    super.dispose();
+  }
+
   Widget _buildHeader(
       BuildContext context, AFBoardColumnHeaderData headerData) {
     return AppFlowyColumnHeader(
@@ -138,7 +180,8 @@ class BoardContent extends StatelessWidget {
     AFBoardColumnData column,
     AFColumnItem columnItem,
   ) {
-    final rowPB = (columnItem as BoardColumnItem).row;
+    final boardColumnItem = columnItem as BoardColumnItem;
+    final rowPB = boardColumnItem.row;
     final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
 
     /// Return placeholder widget if the rowCache is null.
@@ -155,16 +198,17 @@ class BoardContent extends StatelessWidget {
     final cellBuilder = BoardCellBuilder(cardController);
     final isEditing = context.read<BoardBloc>().state.editingRow.fold(
           () => false,
-          (editingRow) => editingRow.id == rowPB.id,
+          (editingRow) => editingRow.row.id == rowPB.id,
         );
 
     return AppFlowyColumnItemCard(
-      key: ObjectKey(columnItem),
+      key: ValueKey(columnItem.id),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
       child: BoardCard(
         gridId: gridId,
         groupId: column.id,
+        fieldId: boardColumnItem.fieldId,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,

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

@@ -35,8 +35,9 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
-        buildWhen: (previous, current) =>
-            previous.selectedOptions != current.selectedOptions,
+        buildWhen: (previous, current) {
+          return previous.selectedOptions != current.selectedOptions;
+        },
         builder: (context, state) {
           if (state.selectedOptions
               .where((element) => element.id == widget.groupId)

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

@@ -15,6 +15,7 @@ typedef OnEndEditing = void Function(String rowId);
 class BoardCard extends StatefulWidget {
   final String gridId;
   final String groupId;
+  final String fieldId;
   final bool isEditing;
   final CardDataController dataController;
   final BoardCellBuilder cellBuilder;
@@ -24,6 +25,7 @@ class BoardCard extends StatefulWidget {
   const BoardCard({
     required this.gridId,
     required this.groupId,
+    required this.fieldId,
     required this.isEditing,
     required this.dataController,
     required this.cellBuilder,
@@ -43,6 +45,7 @@ class _BoardCardState extends State<BoardCard> {
   void initState() {
     _cardBloc = BoardCardBloc(
       gridId: widget.gridId,
+      fieldId: widget.fieldId,
       dataController: widget.dataController,
     )..add(const BoardCardEvent.initial());
     super.initState();
@@ -53,6 +56,9 @@ class _BoardCardState extends State<BoardCard> {
     return BlocProvider.value(
       value: _cardBloc,
       child: BlocBuilder<BoardCardBloc, BoardCardState>(
+        buildWhen: (previous, current) {
+          return previous.cells.length != current.cells.length;
+        },
         builder: (context, state) {
           return BoardCardContainer(
             accessoryBuilder: (context) {
@@ -62,7 +68,10 @@ class _BoardCardState extends State<BoardCard> {
               widget.openCard(context);
             },
             child: Column(
-              children: _makeCells(context, state.gridCellMap),
+              children: _makeCells(
+                context,
+                state.cells.map((cell) => cell.identifier).toList(),
+              ),
             ),
           );
         },
@@ -70,9 +79,12 @@ class _BoardCardState extends State<BoardCard> {
     );
   }
 
-  List<Widget> _makeCells(BuildContext context, GridCellMap cellMap) {
-    return cellMap.values.map(
-      (cellId) {
+  List<Widget> _makeCells(
+    BuildContext context,
+    List<GridCellIdentifier> cells,
+  ) {
+    return cells.map(
+      (GridCellIdentifier cellId) {
         final child = widget.cellBuilder.buildCell(widget.groupId, cellId);
         return Padding(
           padding: const EdgeInsets.only(left: 4, right: 4, top: 6),

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -35,7 +35,7 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
 
   void show(BuildContext context) async {
     final windowSize = MediaQuery.of(context).size;
-    final size = windowSize * 0.7;
+    final size = windowSize * 0.5;
     FlowyOverlay.of(context).insertWithRect(
       widget: OverlayContainer(
         child: this,

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

@@ -114,7 +114,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
       return Align(
         alignment: Alignment.centerLeft,
         child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60),
+          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
           child: Text(item.s),
         ),
       );
@@ -124,7 +124,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
       return Align(
         alignment: Alignment.centerLeft,
         child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60),
+          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
           child: Column(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [

+ 119 - 19
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -1,13 +1,27 @@
+import 'package:appflowy_board/src/utils/log.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'board_column/board_column.dart';
 import 'board_column/board_column_data.dart';
 import 'board_data.dart';
+import 'reorder_flex/drag_state.dart';
 import 'reorder_flex/drag_target_interceptor.dart';
 import 'reorder_flex/reorder_flex.dart';
 import 'reorder_phantom/phantom_controller.dart';
 import '../rendering/board_overlay.dart';
 
+class AFBoardScrollManager {
+  BoardColumnsState? _columnState;
+
+  // AFBoardScrollManager();
+
+  void scrollToBottom(String columnId, VoidCallback? completed) {
+    _columnState
+        ?.getReorderFlexState(columnId: columnId)
+        ?.scrollToBottom(completed);
+  }
+}
+
 class AFBoardConfig {
   final double cornerRadius;
   final EdgeInsets columnPadding;
@@ -56,6 +70,10 @@ class AFBoard extends StatelessWidget {
 
   final AFBoardConfig config;
 
+  final AFBoardScrollManager? scrollManager;
+
+  final BoardColumnsState _columnState = BoardColumnsState();
+
   AFBoard({
     required this.dataController,
     required this.cardBuilder,
@@ -63,6 +81,7 @@ class AFBoard extends StatelessWidget {
     this.footBuilder,
     this.headerBuilder,
     this.scrollController,
+    this.scrollManager,
     this.columnConstraints = const BoxConstraints(maxWidth: 200),
     this.config = const AFBoardConfig(),
     Key? key,
@@ -75,10 +94,16 @@ class AFBoard extends StatelessWidget {
       value: dataController,
       child: Consumer<AFBoardDataController>(
         builder: (context, notifier, child) {
-          return BoardContent(
+          if (scrollManager != null) {
+            scrollManager!._columnState = _columnState;
+          }
+
+          return AFBoardContent(
             config: config,
             dataController: dataController,
             scrollController: scrollController,
+            scrollManager: scrollManager,
+            columnsState: _columnState,
             background: background,
             delegate: phantomController,
             columnConstraints: columnConstraints,
@@ -94,7 +119,7 @@ class AFBoard extends StatelessWidget {
   }
 }
 
-class BoardContent extends StatefulWidget {
+class AFBoardContent extends StatefulWidget {
   final ScrollController? scrollController;
   final OnDragStarted? onDragStarted;
   final OnReorder onReorder;
@@ -104,6 +129,8 @@ class BoardContent extends StatefulWidget {
   final AFBoardConfig config;
   final ReorderFlexConfig reorderFlexConfig;
   final BoxConstraints columnConstraints;
+  final AFBoardScrollManager? scrollManager;
+  final BoardColumnsState columnsState;
 
   ///
   final AFBoardColumnCardBuilder cardBuilder;
@@ -118,11 +145,13 @@ class BoardContent extends StatefulWidget {
 
   final BoardPhantomController phantomController;
 
-  const BoardContent({
+  const AFBoardContent({
     required this.config,
     required this.onReorder,
     required this.delegate,
     required this.dataController,
+    required this.scrollManager,
+    required this.columnsState,
     this.onDragStarted,
     this.onDragEnded,
     this.scrollController,
@@ -137,12 +166,12 @@ class BoardContent extends StatefulWidget {
         super(key: key);
 
   @override
-  State<BoardContent> createState() => _BoardContentState();
+  State<AFBoardContent> createState() => _AFBoardContentState();
 }
 
-class _BoardContentState extends State<BoardContent> {
-  final GlobalKey _columnContainerOverlayKey =
-      GlobalKey(debugLabel: '$BoardContent overlay key');
+class _AFBoardContentState extends State<AFBoardContent> {
+  final GlobalKey _boardContentKey =
+      GlobalKey(debugLabel: '$AFBoardContent overlay key');
   late BoardOverlayEntry _overlayEntry;
 
   @override
@@ -153,10 +182,10 @@ class _BoardContentState extends State<BoardContent> {
           reorderFlexId: widget.dataController.identifier,
           acceptedReorderFlexId: widget.dataController.columnIds,
           delegate: widget.delegate,
+          columnsState: widget.columnsState,
         );
 
         final reorderFlex = ReorderFlex(
-          key: widget.key,
           config: widget.reorderFlexConfig,
           scrollController: widget.scrollController,
           onDragStarted: widget.onDragStarted,
@@ -165,7 +194,7 @@ class _BoardContentState extends State<BoardContent> {
           dataSource: widget.dataController,
           direction: Axis.horizontal,
           interceptor: interceptor,
-          children: _buildColumns(interceptor.columnKeys),
+          children: _buildColumns(),
         );
 
         return Stack(
@@ -192,12 +221,12 @@ class _BoardContentState extends State<BoardContent> {
   @override
   Widget build(BuildContext context) {
     return BoardOverlay(
-      key: _columnContainerOverlayKey,
+      key: _boardContentKey,
       initialEntries: [_overlayEntry],
     );
   }
 
-  List<Widget> _buildColumns(List<ColumnKey> columnKeys) {
+  List<Widget> _buildColumns() {
     final List<Widget> children =
         widget.dataController.columnDatas.asMap().entries.map(
       (item) {
@@ -215,6 +244,8 @@ class _BoardContentState extends State<BoardContent> {
           child: Consumer<AFBoardColumnDataController>(
             builder: (context, value, child) {
               final boardColumn = AFBoardColumnWidget(
+                key: PageStorageKey<String>(columnData.id),
+                // key: GlobalObjectKey(columnData.id),
                 margin: _marginFromIndex(columnIndex),
                 itemMargin: widget.config.columnItemPadding,
                 headerBuilder: _buildHeader,
@@ -226,16 +257,11 @@ class _BoardContentState extends State<BoardContent> {
                 onReorder: widget.dataController.moveColumnItem,
                 cornerRadius: widget.config.cornerRadius,
                 backgroundColor: widget.config.columnBackgroundColor,
+                dragStateStorage: widget.columnsState,
+                dragTargetIndexKeyStorage: widget.columnsState,
               );
 
-              // columnKeys
-              //     .removeWhere((element) => element.columnId == columnData.id);
-              // columnKeys.add(
-              //   ColumnKey(
-              //     columnId: columnData.id,
-              //     key: boardColumn.columnGlobalKey,
-              //   ),
-              // );
+              widget.columnsState.addColumn(columnData.id, boardColumn);
 
               return ConstrainedBox(
                 constraints: widget.columnConstraints,
@@ -296,3 +322,77 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource {
   @override
   List<String> get acceptedColumnIds => dataController.columnIds;
 }
+
+class BoardColumnContext {
+  GlobalKey? columnKey;
+  DraggingState? draggingState;
+}
+
+class BoardColumnsState extends DraggingStateStorage
+    with ReorderDragTargetIndexKeyStorage {
+  /// Quick access to the [AFBoardColumnWidget]
+  final Map<String, GlobalKey> columnKeys = {};
+  final Map<String, DraggingState> columnDragStates = {};
+  final Map<String, Map<String, GlobalObjectKey>> columnDragDragTargets = {};
+
+  void addColumn(String columnId, AFBoardColumnWidget columnWidget) {
+    columnKeys[columnId] = columnWidget.globalKey;
+  }
+
+  ReorderFlexState? getReorderFlexState({required String columnId}) {
+    final flexGlobalKey = columnKeys[columnId];
+    if (flexGlobalKey == null) return null;
+    if (flexGlobalKey.currentState is! ReorderFlexState) return null;
+    final state = flexGlobalKey.currentState as ReorderFlexState;
+    return state;
+  }
+
+  ReorderFlex? getReorderFlex({required String columnId}) {
+    final flexGlobalKey = columnKeys[columnId];
+    if (flexGlobalKey == null) return null;
+    if (flexGlobalKey.currentWidget is! ReorderFlex) return null;
+    final widget = flexGlobalKey.currentWidget as ReorderFlex;
+    return widget;
+  }
+
+  @override
+  DraggingState? read(String reorderFlexId) {
+    return columnDragStates[reorderFlexId];
+  }
+
+  @override
+  void write(String reorderFlexId, DraggingState state) {
+    Log.trace('$reorderFlexId Write dragging state: $state');
+    columnDragStates[reorderFlexId] = state;
+  }
+
+  @override
+  void remove(String reorderFlexId) {
+    columnDragStates.remove(reorderFlexId);
+  }
+
+  @override
+  void addKey(
+    String reorderFlexId,
+    String key,
+    GlobalObjectKey<State<StatefulWidget>> value,
+  ) {
+    Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
+    if (column == null) {
+      column = {};
+      columnDragDragTargets[reorderFlexId] = column;
+    }
+    column[key] = value;
+  }
+
+  @override
+  GlobalObjectKey<State<StatefulWidget>>? readKey(
+      String reorderFlexId, String key) {
+    Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
+    if (column != null) {
+      return column[key];
+    } else {
+      return null;
+    }
+  }
+}

+ 15 - 13
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 
+import 'package:appflowy_board/src/widgets/reorder_flex/drag_state.dart';
 import 'package:flutter/material.dart';
 import '../../rendering/board_overlay.dart';
 import '../../utils/log.dart';
@@ -65,7 +66,6 @@ class AFBoardColumnWidget extends StatefulWidget {
   final AFBoardColumnDataDataSource dataSource;
   final ScrollController? scrollController;
   final ReorderFlexConfig config;
-
   final OnColumnDragStarted? onDragStarted;
   final OnColumnReorder onReorder;
   final OnColumnDragEnded? onDragEnded;
@@ -88,7 +88,11 @@ class AFBoardColumnWidget extends StatefulWidget {
 
   final Color backgroundColor;
 
-  final GlobalKey columnGlobalKey = GlobalKey();
+  final DraggingStateStorage? dragStateStorage;
+
+  final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
+
+  final GlobalKey globalKey;
 
   AFBoardColumnWidget({
     Key? key,
@@ -98,14 +102,17 @@ class AFBoardColumnWidget extends StatefulWidget {
     required this.onReorder,
     required this.dataSource,
     required this.phantomController,
-    this.onDragStarted,
+    this.dragStateStorage,
+    this.dragTargetIndexKeyStorage,
     this.scrollController,
+    this.onDragStarted,
     this.onDragEnded,
     this.margin = EdgeInsets.zero,
     this.itemMargin = EdgeInsets.zero,
     this.cornerRadius = 0.0,
     this.backgroundColor = Colors.transparent,
-  })  : config = const ReorderFlexConfig(),
+  })  : globalKey = GlobalKey(),
+        config = const ReorderFlexConfig(),
         super(key: key);
 
   @override
@@ -115,7 +122,6 @@ class AFBoardColumnWidget extends StatefulWidget {
 class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
   final GlobalKey _columnOverlayKey =
       GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key');
-
   late BoardOverlayEntry _overlayEntry;
 
   @override
@@ -140,7 +146,9 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
         );
 
         Widget reorderFlex = ReorderFlex(
-          key: widget.columnGlobalKey,
+          key: widget.globalKey,
+          dragStateStorage: widget.dragStateStorage,
+          dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage,
           scrollController: widget.scrollController,
           config: widget.config,
           onDragStarted: (index) {
@@ -163,9 +171,6 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
           children: children,
         );
 
-        // reorderFlex =
-        //     KeyedSubtree(key: widget.columnGlobalKey, child: reorderFlex);
-
         return Container(
           margin: widget.margin,
           clipBehavior: Clip.hardEdge,
@@ -177,10 +182,7 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
             children: [
               if (header != null) header,
               Expanded(
-                child: Padding(
-                  padding: widget.itemMargin,
-                  child: reorderFlex,
-                ),
+                child: Padding(padding: widget.itemMargin, child: reorderFlex),
               ),
               if (footer != null) footer,
             ],

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

@@ -196,7 +196,6 @@ class AFBoardDataController extends ChangeNotifier
     final index =
         columnDataController.items.indexWhere((item) => item.isPhantom);
 
-    assert(index != -1);
     if (index != -1) {
       if (index != newIndex) {
         Log.trace(

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

@@ -24,6 +24,10 @@ class FlexDragTargetData extends DragTargetData {
 
   final String dragTargetId;
 
+  Offset dragTargetOffset = Offset.zero;
+
+  final GlobalObjectKey dragTargetIndexKey;
+
   final String reorderFlexId;
 
   final ReoderFlexItem reorderFlexItem;
@@ -33,6 +37,7 @@ class FlexDragTargetData extends DragTargetData {
     required this.draggingIndex,
     required this.reorderFlexId,
     required this.reorderFlexItem,
+    required this.dragTargetIndexKey,
     required DraggingState state,
   }) : _state = state;
 
@@ -40,6 +45,50 @@ class FlexDragTargetData extends DragTargetData {
   String toString() {
     return 'ReorderFlexId: $reorderFlexId, dragTargetId: $dragTargetId';
   }
+
+  bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) {
+    final renderBox = dragTargetIndexKey.currentContext?.findRenderObject();
+
+    if (renderBox == null) return false;
+    if (renderBox is! RenderBox) return false;
+    final size = feedbackSize ?? Size.zero;
+    final Rect rect = dragTargetOffset & size;
+
+    for (final widgetKey in widgetKeys) {
+      final renderObject = widgetKey.currentContext?.findRenderObject();
+      if (renderObject != null && renderObject is RenderBox) {
+        Rect widgetRect =
+            renderObject.localToGlobal(Offset.zero) & renderObject.size;
+        // return rect.overlaps(widgetRect);
+        if (rect.right <= widgetRect.left || widgetRect.right <= rect.left) {
+          return false;
+        }
+
+        if (rect.bottom <= widgetRect.top || widgetRect.bottom <= rect.top) {
+          return false;
+        }
+        return true;
+      }
+    }
+
+    // final HitTestResult result = HitTestResult();
+    // WidgetsBinding.instance.hitTest(result, position);
+    // for (final HitTestEntry entry in result.path) {
+    //   final HitTestTarget target = entry.target;
+    //   if (target is RenderMetaData) {
+    //     print(target.metaData);
+    //   }
+    //   print(target);
+    // }
+
+    return false;
+  }
+}
+
+abstract class DraggingStateStorage {
+  void write(String reorderFlexId, DraggingState state);
+  void remove(String reorderFlexId);
+  DraggingState? read(String reorderFlexId);
 }
 
 class DraggingState {
@@ -128,6 +177,7 @@ class DraggingState {
 
   /// Set the currentIndex to nextIndex
   void moveDragTargetToNext() {
+    Log.debug('$reorderFlexId updateCurrentIndex: $nextIndex');
     currentIndex = nextIndex;
   }
 
@@ -136,6 +186,14 @@ class DraggingState {
     nextIndex = index;
   }
 
+  void setStartDraggingIndex(int index) {
+    Log.debug('$reorderFlexId setDragIndex: $index');
+    dragStartIndex = index;
+    phantomIndex = index;
+    currentIndex = index;
+    nextIndex = index;
+  }
+
   bool isNotDragging() {
     return dragStartIndex == -1;
   }

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

@@ -26,6 +26,11 @@ typedef DragTargetWillAccepted<T extends DragTargetData> = bool Function(
 ///
 typedef DragTargetOnStarted = void Function(Widget, int, Size?);
 
+typedef DragTargetOnMove<T extends DragTargetData> = void Function(
+  T dragTargetData,
+  Offset offset,
+);
+
 ///
 typedef DragTargetOnEnded<T extends DragTargetData> = void Function(
     T dragTargetData);
@@ -39,13 +44,15 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
   final Widget child;
   final T dragTargetData;
 
-  final GlobalObjectKey _indexGlobalKey;
+  final GlobalObjectKey indexGlobalKey;
 
   /// Called when dragTarget is being dragging.
   final DragTargetOnStarted onDragStarted;
 
   final DragTargetOnEnded<T> onDragEnded;
 
+  final DragTargetOnMove<T> onDragMoved;
+
   /// Called to determine whether this widget is interested in receiving a given
   /// piece of data being dragged over this drag target.
   ///
@@ -69,11 +76,13 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
 
   final bool useMoveAnimation;
 
-  ReorderDragTarget({
+  const ReorderDragTarget({
     Key? key,
     required this.child,
+    required this.indexGlobalKey,
     required this.dragTargetData,
     required this.onDragStarted,
+    required this.onDragMoved,
     required this.onDragEnded,
     required this.onWillAccept,
     required this.insertAnimationController,
@@ -82,8 +91,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
     this.onAccept,
     this.onLeave,
     this.draggableTargetBuilder,
-  })  : _indexGlobalKey = GlobalObjectKey(child.key!),
-        super(key: key);
+  }) : super(key: key);
 
   @override
   State<ReorderDragTarget<T>> createState() => _ReorderDragTargetState<T>();
@@ -104,6 +112,9 @@ class _ReorderDragTargetState<T extends DragTargetData>
         return widget.onWillAccept(dragTargetData);
       },
       onAccept: widget.onAccept,
+      onMove: (detail) {
+        widget.onDragMoved(detail.data, detail.offset);
+      },
       onLeave: (dragTargetData) {
         assert(dragTargetData != null);
         if (dragTargetData != null) {
@@ -112,7 +123,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
       },
     );
 
-    dragTarget = KeyedSubtree(key: widget._indexGlobalKey, child: dragTarget);
+    dragTarget = KeyedSubtree(key: widget.indexGlobalKey, child: dragTarget);
     return dragTarget;
   }
 
@@ -150,7 +161,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
             child: widget.child,
           ),
           onDragStarted: () {
-            _draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size;
+            _draggingFeedbackSize = widget.indexGlobalKey.currentContext?.size;
             widget.onDragStarted(
               widget.child,
               widget.dragTargetData.draggingIndex,

+ 19 - 18
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart

@@ -1,7 +1,7 @@
 import 'dart:async';
 
+import 'package:appflowy_board/src/widgets/board.dart';
 import 'package:flutter/material.dart';
-
 import '../../utils/log.dart';
 import 'drag_state.dart';
 import 'drag_target.dart';
@@ -41,7 +41,7 @@ abstract class OverlapDragTargetDelegate {
     int dragTargetIndex,
   );
 
-  int canMoveTo(String dragTargetId);
+  int getInsertedIndex(String dragTargetId);
 }
 
 /// [OverlappingDragTargetInterceptor] is used to receive the overlapping
@@ -55,13 +55,14 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
   final String reorderFlexId;
   final List<String> acceptedReorderFlexId;
   final OverlapDragTargetDelegate delegate;
-  final List<ColumnKey> columnKeys = [];
+  final BoardColumnsState columnsState;
   Timer? _delayOperation;
 
   OverlappingDragTargetInterceptor({
     required this.delegate,
     required this.reorderFlexId,
     required this.acceptedReorderFlexId,
+    required this.columnsState,
   });
 
   @override
@@ -79,24 +80,30 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
     if (dragTargetId == dragTargetData.reorderFlexId) {
       delegate.cancel();
     } else {
+      // Ignore the event if the dragTarget overlaps with the other column's dragTargets.
+      final columnKeys = columnsState.columnDragDragTargets[dragTargetId];
+      if (columnKeys != null) {
+        final keys = columnKeys.values.toList();
+        if (dragTargetData.isOverlapWithWidgets(keys)) {
+          _delayOperation?.cancel();
+          return true;
+        }
+      }
+
       /// The priority of the column interactions is high than the cross column.
       /// Workaround: delay 100 milliseconds to lower the cross column event priority.
+      ///
       _delayOperation?.cancel();
       _delayOperation = Timer(const Duration(milliseconds: 100), () {
-        final index = delegate.canMoveTo(dragTargetId);
+        final index = delegate.getInsertedIndex(dragTargetId);
         if (index != -1) {
           Log.trace(
               '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
           delegate.moveTo(dragTargetId, dragTargetData, index);
 
-          // final columnIndex = columnKeys
-          //     .indexWhere((element) => element.columnId == dragTargetId);
-          // if (columnIndex != -1) {
-          //   final state = columnKeys[columnIndex].key.currentState;
-          //   if (state is ReorderFlexState) {
-          //     state.handleOnWillAccept(context, index);
-          //   }
-          // }
+          columnsState
+              .getReorderFlexState(columnId: dragTargetId)
+              ?.resetDragTargetIndex(index);
         }
       });
     }
@@ -105,12 +112,6 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
   }
 }
 
-class ColumnKey {
-  String columnId;
-  GlobalKey key;
-  ColumnKey({required this.columnId, required this.key});
-}
-
 abstract class CrossReorderFlexDragTargetDelegate {
   /// * [reorderFlexId] is the id that the [ReorderFlex] passed in.
   bool acceptNewDragTargetData(

+ 101 - 12
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -31,6 +31,11 @@ abstract class ReoderFlexItem {
   String get id;
 }
 
+abstract class ReorderDragTargetIndexKeyStorage {
+  void addKey(String reorderFlexId, String key, GlobalObjectKey value);
+  GlobalObjectKey? readKey(String reorderFlexId, String key);
+}
+
 class ReorderFlexConfig {
   /// The opacity of the dragging widget
   final double draggingWidgetOpacity = 0.3;
@@ -52,7 +57,6 @@ class ReorderFlexConfig {
 
 class ReorderFlex extends StatefulWidget {
   final ReorderFlexConfig config;
-
   final List<Widget> children;
 
   /// [direction] How to place the children, default is Axis.vertical
@@ -74,18 +78,26 @@ class ReorderFlex extends StatefulWidget {
 
   final DragTargetInterceptor? interceptor;
 
-  const ReorderFlex({
+  final DraggingStateStorage? dragStateStorage;
+
+  final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
+
+  ReorderFlex({
     Key? key,
     this.scrollController,
     required this.dataSource,
     required this.children,
     required this.config,
     required this.onReorder,
+    this.dragStateStorage,
+    this.dragTargetIndexKeyStorage,
     this.onDragStarted,
     this.onDragEnded,
     this.interceptor,
     this.direction = Axis.vertical,
-  }) : super(key: key);
+  })  : assert(children.every((Widget w) => w.key != null),
+            'All child must have a key.'),
+        super(key: key);
 
   @override
   State<ReorderFlex> createState() => ReorderFlexState();
@@ -115,7 +127,12 @@ class ReorderFlexState extends State<ReorderFlex>
   @override
   void initState() {
     _notifier = ReorderFlexNotifier();
-    dragState = DraggingState(widget.reorderFlexId);
+    final flexId = widget.reorderFlexId;
+    dragState = widget.dragStateStorage?.read(flexId) ??
+        DraggingState(widget.reorderFlexId);
+    Log.trace('[DragTarget] init dragState: $dragState');
+
+    widget.dragStateStorage?.remove(flexId);
 
     _animation = DragTargetAnimation(
       reorderAnimationDuration: widget.config.reorderAnimationDuration,
@@ -159,7 +176,17 @@ class ReorderFlexState extends State<ReorderFlex>
 
     for (int i = 0; i < widget.children.length; i += 1) {
       Widget child = widget.children[i];
-      children.add(_wrap(child, i));
+      final ReoderFlexItem item = widget.dataSource.items[i];
+
+      final indexKey = GlobalObjectKey(child.key!);
+      // Save the index key for quick access
+      widget.dragTargetIndexKeyStorage?.addKey(
+        widget.reorderFlexId,
+        item.id,
+        indexKey,
+      );
+
+      children.add(_wrap(child, i, indexKey));
 
       // if (widget.config.useMovePlaceholder) {
       //   children.add(DragTargeMovePlaceholder(
@@ -203,10 +230,10 @@ class ReorderFlexState extends State<ReorderFlex>
 
   /// [child]: the child will be wrapped with dartTarget
   /// [childIndex]: the index of the child in a list
-  Widget _wrap(Widget child, int childIndex) {
+  Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
     return Builder(builder: (context) {
       final ReorderDragTarget dragTarget =
-          _buildDragTarget(context, child, childIndex);
+          _buildDragTarget(context, child, childIndex, indexKey);
       int shiftedIndex = childIndex;
 
       if (dragState.isOverlapWithPhantom()) {
@@ -312,22 +339,31 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   ReorderDragTarget _buildDragTarget(
-      BuildContext builderContext, Widget child, int dragTargetIndex) {
-    final ReoderFlexItem reorderFlexItem =
-        widget.dataSource.items[dragTargetIndex];
+    BuildContext builderContext,
+    Widget child,
+    int dragTargetIndex,
+    GlobalObjectKey indexKey,
+  ) {
+    final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
     return ReorderDragTarget<FlexDragTargetData>(
+      indexGlobalKey: indexKey,
       dragTargetData: FlexDragTargetData(
         draggingIndex: dragTargetIndex,
         reorderFlexId: widget.reorderFlexId,
         reorderFlexItem: reorderFlexItem,
         state: dragState,
         dragTargetId: reorderFlexItem.id,
+        dragTargetIndexKey: indexKey,
       ),
       onDragStarted: (draggingWidget, draggingIndex, size) {
         Log.debug(
             "[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
         _startDragging(draggingWidget, draggingIndex, size);
         widget.onDragStarted?.call(draggingIndex);
+        widget.dragStateStorage?.remove(widget.reorderFlexId);
+      },
+      onDragMoved: (dragTargetData, offset) {
+        dragTargetData.dragTargetOffset = offset;
       },
       onDragEnded: (dragTargetData) {
         if (!mounted) return;
@@ -431,14 +467,20 @@ class ReorderFlexState extends State<ReorderFlex>
     });
   }
 
+  void resetDragTargetIndex(int dragTargetIndex) {
+    dragState.setStartDraggingIndex(dragTargetIndex);
+    widget.dragStateStorage?.write(
+      widget.reorderFlexId,
+      dragState,
+    );
+  }
+
   bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
     final dragIndex = dragState.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.
     ///
-    Log.trace(
-        '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}');
 
     bool willAccept =
         dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex;
@@ -452,6 +494,9 @@ class ReorderFlexState extends State<ReorderFlex>
       _requestAnimationToNextIndex(isAcceptingNewTarget: true);
     });
 
+    Log.trace(
+        '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}');
+
     _scrollTo(context);
 
     /// If the target is not the original starting point, then we will accept the drop.
@@ -515,6 +560,50 @@ class ReorderFlexState extends State<ReorderFlex>
     }
   }
 
+  void scrollToBottom(VoidCallback? completed) {
+    if (_scrolling) {
+      completed?.call();
+      return;
+    }
+
+    if (widget.dataSource.items.isNotEmpty) {
+      final item = widget.dataSource.items.last;
+      final indexKey = widget.dragTargetIndexKeyStorage?.readKey(
+        widget.reorderFlexId,
+        item.id,
+      );
+      if (indexKey == null) {
+        completed?.call();
+        return;
+      }
+
+      final indexContext = indexKey.currentContext;
+      if (indexContext == null || _scrollController.hasClients == false) {
+        completed?.call();
+        return;
+      }
+
+      final renderObject = indexContext.findRenderObject();
+      if (renderObject != null) {
+        _scrolling = true;
+        _scrollController.position
+            .ensureVisible(
+          renderObject,
+          alignment: 0.5,
+          duration: const Duration(milliseconds: 120),
+        )
+            .then((value) {
+          setState(() {
+            _scrolling = false;
+            completed?.call();
+          });
+        });
+      } else {
+        completed?.call();
+      }
+    }
+  }
+
 // Scrolls to a target context if that context is not on the screen.
   void _scrollTo(BuildContext context) {
     if (_scrolling) return;

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

@@ -203,7 +203,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
   }
 
   @override
-  int canMoveTo(String dragTargetId) {
+  int getInsertedIndex(String dragTargetId) {
     if (columnsState.isDragging(dragTargetId)) {
       return -1;
     }

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

@@ -18,7 +18,7 @@ use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeseri
 use flowy_sync::entities::revision::Revision;
 use flowy_sync::errors::CollaborateResult;
 use flowy_sync::util::make_text_delta_from_revisions;
-use lib_infra::future::FutureResult;
+use lib_infra::future::{wrap_future, FutureResult};
 
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -591,34 +591,33 @@ impl GridRevisionEditor {
         match self.block_manager.get_row_rev(&from_row_id).await? {
             None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id),
             Some(row_rev) => {
-                if let Some(row_changeset) = self
-                    .view_manager
-                    .move_group_row(row_rev, to_group_id, to_row_id.clone())
-                    .await
-                {
-                    tracing::trace!("Move group row cause row data changed: {:?}", row_changeset);
-
-                    let cell_changesets = row_changeset
-                        .cell_by_field_id
-                        .into_iter()
-                        .map(|(field_id, cell_rev)| CellChangesetPB {
-                            grid_id: view_id.clone(),
-                            row_id: row_changeset.row_id.clone(),
-                            field_id,
-                            content: cell_rev.data,
+                let block_manager = self.block_manager.clone();
+                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);
+                            let cell_changesets = row_changeset
+                                .cell_by_field_id
+                                .into_iter()
+                                .map(|(field_id, cell_rev)| CellChangesetPB {
+                                    grid_id: view_id.clone(),
+                                    row_id: row_changeset.row_id.clone(),
+                                    field_id,
+                                    content: cell_rev.data,
+                                })
+                                .collect::<Vec<CellChangesetPB>>();
+
+                            for cell_changeset in cell_changesets {
+                                match block_manager.update_cell(cell_changeset).await {
+                                    Ok(_) => {}
+                                    Err(e) => tracing::error!("Apply cell changeset error:{:?}", e),
+                                }
+                            }
                         })
-                        .collect::<Vec<CellChangesetPB>>();
-
-                    for cell_changeset in cell_changesets {
-                        match self.block_manager.update_cell(cell_changeset).await {
-                            Ok(_) => {}
-                            Err(e) => tracing::error!("Apply cell changeset error:{:?}", e),
-                        }
-                    }
-                }
+                    })
+                    .await?;
             }
         }
-
         Ok(())
     }
 

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

@@ -140,8 +140,8 @@ impl GridViewRevisionEditor {
         row_changeset: &mut RowChangeset,
         to_group_id: &str,
         to_row_id: Option<String>,
-    ) {
-        if let Some(changesets) = self
+    ) -> Vec<GroupChangesetPB> {
+        match self
             .group_service
             .write()
             .await
@@ -150,9 +150,8 @@ impl GridViewRevisionEditor {
             })
             .await
         {
-            for changeset in changesets {
-                self.notify_did_update_group(changeset).await;
-            }
+            None => vec![],
+            Some(changesets) => changesets,
         }
     }
     /// Only call once after grid view editor initialized
@@ -266,7 +265,7 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
-    async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
+    pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
         send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup)
             .payload(changeset)
             .send();

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

@@ -134,19 +134,23 @@ impl GridViewManager {
         row_rev: Arc<RowRevision>,
         to_group_id: String,
         to_row_id: Option<String>,
-    ) -> Option<RowChangeset> {
+        with_row_changeset: impl FnOnce(RowChangeset) -> AFFuture<()>,
+    ) -> FlowyResult<()> {
         let mut row_changeset = RowChangeset::new(row_rev.id.clone());
-        for view_editor in self.view_editors.iter() {
-            view_editor
-                .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
-                .await;
+        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())
+            .await;
+
+        if row_changeset.is_empty() == false {
+            with_row_changeset(row_changeset).await;
         }
 
-        if row_changeset.is_empty() {
-            None
-        } else {
-            Some(row_changeset)
+        for group_changeset in group_changesets {
+            view_editor.notify_did_update_group(group_changeset).await;
         }
+
+        Ok(())
     }
 
     pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {