浏览代码

feat: reorder rows in grid (#2533)

* feat: reorder rows in grid

Allows reordering of rows in a Grid where no current filter and sorting is applied

Closes: #1123

* fix: resolve review comments
Mathias Mogensen 2 年之前
父节点
当前提交
f8f0599462
共有 16 个文件被更改,包括 315 次插入196 次删除
  1. 1 0
      frontend/appflowy_flutter/assets/translations/en.json
  2. 12 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  3. 13 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  4. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  5. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  6. 12 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  7. 102 77
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  8. 57 32
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart
  9. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  10. 6 9
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart
  11. 5 5
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart
  12. 33 35
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
  13. 21 25
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart
  14. 2 2
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart
  15. 15 4
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart
  16. 31 0
      frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart

+ 1 - 0
frontend/appflowy_flutter/assets/translations/en.json

@@ -108,6 +108,7 @@
     "openAsPage": "Open as a Page",
     "addNewRow": "Add a new row",
     "openMenu": "Click to open menu",
+    "dragRow": "Long press to reorder the row",
     "viewDataBase": "View database",
     "referencePage": "This {name} is referenced"
   },

+ 12 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -175,18 +175,28 @@ class DatabaseController {
     );
   }
 
-  Future<Either<Unit, FlowyError>> moveRow({
+  Future<Either<Unit, FlowyError>> moveGroupRow({
     required RowPB fromRow,
     required String groupId,
     RowPB? toRow,
   }) {
-    return _databaseViewBackendSvc.moveRow(
+    return _databaseViewBackendSvc.moveGroupRow(
       fromRowId: fromRow.id,
       toGroupId: groupId,
       toRowId: toRow?.id,
     );
   }
 
+  Future<Either<Unit, FlowyError>> moveRow({
+    required RowPB fromRow,
+    required RowPB toRow,
+  }) {
+    return _databaseViewBackendSvc.moveRow(
+      fromRowId: fromRow.id,
+      toRowId: toRow.id,
+    );
+  }
+
   Future<Either<Unit, FlowyError>> moveGroup({
     required String fromGroupId,
     required String toGroupId,

+ 13 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart

@@ -44,7 +44,7 @@ class DatabaseViewBackendService {
     return DatabaseEventCreateRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> moveRow({
+  Future<Either<Unit, FlowyError>> moveGroupRow({
     required String fromRowId,
     required String toGroupId,
     String? toRowId,
@@ -61,6 +61,18 @@ class DatabaseViewBackendService {
     return DatabaseEventMoveGroupRow(payload).send();
   }
 
+  Future<Either<Unit, FlowyError>> moveRow({
+    required String fromRowId,
+    required String toRowId,
+  }) {
+    var payload = MoveRowPayloadPB.create()
+      ..viewId = viewId
+      ..fromRowId = fromRowId
+      ..toRowId = toRowId;
+
+    return DatabaseEventMoveRow(payload).send();
+  }
+
   Future<Either<Unit, FlowyError>> moveGroup({
     required String fromGroupId,
     required String toGroupId,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart

@@ -29,7 +29,7 @@ abstract class RowCacheDelegate {
 class RowCache {
   final String viewId;
 
-  /// _rows containers the current block's rows
+  /// _rows contains the current block's rows
   /// Use List to reverse the order of the GridRow.
   final RowList _rowList = RowList();
 

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -53,7 +53,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
-          _databaseController.moveRow(
+          _databaseController.moveGroupRow(
             fromRow: fromRow,
             toRow: toRow,
             groupId: groupId,
@@ -69,7 +69,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
-          _databaseController.moveRow(
+          _databaseController.moveGroupRow(
             fromRow: fromRow,
             toRow: toRow,
             groupId: toGroupId,

+ 12 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -35,6 +35,17 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             );
             await rowService.deleteRow(rowInfo.rowPB.id);
           },
+          moveRow: (int from, int to) {
+            final List<RowInfo> rows = [...state.rowInfos];
+
+            final fromRow = rows[from].rowPB;
+            final toRow = rows[to].rowPB;
+
+            rows.insert(to, rows.removeAt(from));
+            emit(state.copyWith(rowInfos: rows));
+
+            databaseController.moveRow(fromRow: fromRow, toRow: toRow);
+          },
           didReceiveGridUpdate: (grid) {
             emit(state.copyWith(grid: Some(grid)));
           },
@@ -110,6 +121,7 @@ class GridEvent with _$GridEvent {
   const factory GridEvent.initial() = InitialGrid;
   const factory GridEvent.createRow() = _CreateRow;
   const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
+  const factory GridEvent.moveRow(int from, int to) = _MoveRow;
   const factory GridEvent.didReceiveRowUpdate(
     List<RowInfo> rows,
     RowsChangedReason listState,

+ 102 - 77
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart

@@ -91,21 +91,6 @@ class _GridPageState extends State<GridPage> {
       ),
     );
   }
-
-  @override
-  void dispose() {
-    super.dispose();
-  }
-
-  @override
-  void deactivate() {
-    super.deactivate();
-  }
-
-  @override
-  void didUpdateWidget(covariant GridPage oldWidget) {
-    super.didUpdateWidget(oldWidget);
-  }
 }
 
 class FlowyGrid extends StatefulWidget {
@@ -119,12 +104,12 @@ class _FlowyGridState extends State<FlowyGrid> {
   final _scrollController = GridScrollController(
     scrollGroupController: LinkedScrollControllerGroup(),
   );
-  late ScrollController headerScrollController;
+  late final ScrollController headerScrollController;
 
   @override
   void initState() {
-    headerScrollController = _scrollController.linkHorizontalController();
     super.initState();
+    headerScrollController = _scrollController.linkHorizontalController();
   }
 
   @override
@@ -216,49 +201,78 @@ class _GridRowsState extends State<_GridRows> {
 
   @override
   Widget build(BuildContext context) {
-    return BlocConsumer<GridBloc, GridState>(
-      listenWhen: (previous, current) => previous.reason != current.reason,
-      listener: (context, state) {
-        state.reason.whenOrNull(
-          insert: (item) {
-            _key.currentState?.insertItem(item.index);
-          },
-          delete: (item) {
-            _key.currentState?.removeItem(
-              item.index,
-              (context, animation) =>
-                  _renderRow(context, item.rowInfo, animation),
+    return Builder(
+      builder: (context) {
+        final filterState = context.watch<GridFilterMenuBloc>().state;
+        final sortState = context.watch<SortMenuBloc>().state;
+
+        return BlocConsumer<GridBloc, GridState>(
+          listenWhen: (previous, current) => previous.reason != current.reason,
+          listener: (context, state) {
+            state.reason.whenOrNull(
+              insert: (item) {
+                _key.currentState?.insertItem(item.index);
+              },
+              delete: (item) {
+                _key.currentState?.removeItem(
+                  item.index,
+                  (context, animation) => _renderRow(
+                    context,
+                    item.rowInfo,
+                    animation: animation,
+                  ),
+                );
+              },
             );
           },
-          reorderSingleRow: (reorderRow, rowInfo) {
-            // _key.currentState?.removeItem(
-            //   reorderRow.oldIndex,
-            //   (context, animation) => _renderRow(context, rowInfo, animation),
-            // );
-            // _key.currentState?.insertItem(reorderRow.newIndex);
-          },
-        );
-      },
-      buildWhen: (previous, current) {
-        return current.reason.whenOrNull(
+          buildWhen: (previous, current) {
+            return current.reason.maybeWhen(
               reorderRows: () => true,
               reorderSingleRow: (reorderRow, rowInfo) => true,
-            ) ??
-            false;
-      },
-      builder: (context, state) {
-        return SliverAnimatedList(
-          key: _key,
-          initialItemCount: context.read<GridBloc>().state.rowInfos.length,
-          itemBuilder:
-              (BuildContext context, int index, Animation<double> animation) {
-            final rowInfos = context.read<GridBloc>().state.rowInfos;
-            if (index >= rowInfos.length) {
-              return const SizedBox();
-            } else {
-              final RowInfo rowInfo = rowInfos[index];
-              return _renderRow(context, rowInfo, animation);
-            }
+              delete: (item) => true,
+              insert: (item) => true,
+              orElse: () => false,
+            );
+          },
+          builder: (context, state) {
+            final rowInfos = context.watch<GridBloc>().state.rowInfos;
+
+            return SliverFillRemaining(
+              child: ReorderableListView.builder(
+                key: _key,
+                buildDefaultDragHandles: false,
+                proxyDecorator: (child, index, animation) => Material(
+                  color: Colors.white.withOpacity(.1),
+                  child: Opacity(
+                    opacity: .5,
+                    child: child,
+                  ),
+                ),
+                onReorder: (fromIndex, newIndex) {
+                  final toIndex =
+                      newIndex > fromIndex ? newIndex - 1 : newIndex;
+
+                  if (fromIndex == toIndex) {
+                    return;
+                  }
+
+                  context
+                      .read<GridBloc>()
+                      .add(GridEvent.moveRow(fromIndex, toIndex));
+                },
+                itemCount: rowInfos.length,
+                itemBuilder: (BuildContext context, int index) {
+                  final RowInfo rowInfo = rowInfos[index];
+                  return _renderRow(
+                    context,
+                    rowInfo,
+                    index: index,
+                    isSortEnabled: sortState.sortInfos.isNotEmpty,
+                    isFilterEnabled: filterState.filters.isNotEmpty,
+                  );
+                },
+              ),
+            );
           },
         );
       },
@@ -267,16 +281,19 @@ class _GridRowsState extends State<_GridRows> {
 
   Widget _renderRow(
     BuildContext context,
-    RowInfo rowInfo,
-    Animation<double> animation,
-  ) {
+    RowInfo rowInfo, {
+    int? index,
+    bool isSortEnabled = false,
+    bool isFilterEnabled = false,
+    Animation<double>? animation,
+  }) {
     final rowCache = context.read<GridBloc>().getRowCache(
           rowInfo.rowPB.blockId,
           rowInfo.rowPB.id,
         );
 
     /// Return placeholder widget if the rowCache is null.
-    if (rowCache == null) return const SizedBox();
+    if (rowCache == null) return const SizedBox.shrink();
 
     final fieldController =
         context.read<GridBloc>().databaseController.fieldController;
@@ -286,24 +303,32 @@ class _GridRowsState extends State<_GridRows> {
       rowCache: rowCache,
     );
 
-    return SizeTransition(
-      sizeFactor: animation,
-      child: GridRow(
-        rowInfo: rowInfo,
-        dataController: dataController,
-        cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
-        openDetailPage: (context, cellBuilder) {
-          _openRowDetailPage(
-            context,
-            rowInfo,
-            fieldController,
-            rowCache,
-            cellBuilder,
-          );
-        },
-        key: ValueKey(rowInfo.rowPB.id),
-      ),
+    final child = GridRow(
+      key: ValueKey(rowInfo.rowPB.id),
+      index: index,
+      isDraggable: !isSortEnabled && !isFilterEnabled,
+      rowInfo: rowInfo,
+      dataController: dataController,
+      cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
+      openDetailPage: (context, cellBuilder) {
+        _openRowDetailPage(
+          context,
+          rowInfo,
+          fieldController,
+          rowCache,
+          cellBuilder,
+        );
+      },
     );
+
+    if (animation != null) {
+      return SizeTransition(
+        sizeFactor: animation,
+        child: child,
+      );
+    }
+
+    return child;
   }
 
   void _openRowDetailPage(

+ 57 - 32
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart

@@ -25,29 +25,34 @@ class GridRow extends StatefulWidget {
   final GridCellBuilder cellBuilder;
   final void Function(BuildContext, GridCellBuilder) openDetailPage;
 
+  final int? index;
+  final bool isDraggable;
+
   const GridRow({
+    super.key,
     required this.rowInfo,
     required this.dataController,
     required this.cellBuilder,
     required this.openDetailPage,
-    Key? key,
-  }) : super(key: key);
+    this.index,
+    this.isDraggable = false,
+  });
 
   @override
   State<GridRow> createState() => _GridRowState();
 }
 
 class _GridRowState extends State<GridRow> {
-  late RowBloc _rowBloc;
+  late final RowBloc _rowBloc;
 
   @override
   void initState() {
+    super.initState();
     _rowBloc = RowBloc(
       rowInfo: widget.rowInfo,
       dataController: widget.dataController,
     );
     _rowBloc.add(const RowEvent.initial());
-    super.initState();
   }
 
   @override
@@ -70,9 +75,11 @@ class _GridRowState extends State<GridRow> {
 
             return Row(
               children: [
-                const _RowLeading(),
+                _RowLeading(
+                  index: widget.index,
+                  isDraggable: widget.isDraggable,
+                ),
                 content,
-                const _RowTrailing(),
               ],
             );
           },
@@ -89,19 +96,25 @@ class _GridRowState extends State<GridRow> {
 }
 
 class _RowLeading extends StatefulWidget {
-  const _RowLeading({Key? key}) : super(key: key);
+  final int? index;
+  final bool isDraggable;
+
+  const _RowLeading({
+    this.index,
+    this.isDraggable = false,
+  });
 
   @override
   State<_RowLeading> createState() => _RowLeadingState();
 }
 
 class _RowLeadingState extends State<_RowLeading> {
-  late PopoverController popoverController;
+  late final PopoverController popoverController;
 
   @override
   void initState() {
-    popoverController = PopoverController();
     super.initState();
+    popoverController = PopoverController();
   }
 
   @override
@@ -131,23 +144,28 @@ class _RowLeadingState extends State<_RowLeading> {
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
         const _InsertButton(),
-        _MenuButton(
-          openMenu: () {
-            popoverController.show();
-          },
-        ),
+        if (isDraggable) ...[
+          ReorderableDragStartListener(
+            index: widget.index!,
+            child: _MenuButton(
+              isDragEnabled: isDraggable,
+              openMenu: () {
+                popoverController.show();
+              },
+            ),
+          ),
+        ] else ...[
+          _MenuButton(
+            openMenu: () {
+              popoverController.show();
+            },
+          ),
+        ],
       ],
     );
   }
-}
 
-class _RowTrailing extends StatelessWidget {
-  const _RowTrailing({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return const SizedBox();
-  }
+  bool get isDraggable => widget.index != null && widget.isDraggable;
 }
 
 class _InsertButton extends StatelessWidget {
@@ -172,25 +190,31 @@ class _InsertButton extends StatelessWidget {
 
 class _MenuButton extends StatefulWidget {
   final VoidCallback openMenu;
+  final bool isDragEnabled;
+
   const _MenuButton({
     required this.openMenu,
-    Key? key,
-  }) : super(key: key);
+    this.isDragEnabled = false,
+  });
 
   @override
   State<_MenuButton> createState() => _MenuButtonState();
 }
 
 class _MenuButtonState extends State<_MenuButton> {
-  @override
-  void initState() {
-    super.initState();
-  }
-
   @override
   Widget build(BuildContext context) {
     return FlowyIconButton(
-      tooltipText: LocaleKeys.tooltip_openMenu.tr(),
+      tooltipText:
+          widget.isDragEnabled ? null : LocaleKeys.tooltip_openMenu.tr(),
+      richTooltipText: widget.isDragEnabled
+          ? TextSpan(
+              children: [
+                TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'),
+                TextSpan(text: LocaleKeys.tooltip_openMenu.tr()),
+              ],
+            )
+          : null,
       hoverColor: AFThemeExtension.of(context).lightGreyHover,
       width: 20,
       height: 30,
@@ -258,6 +282,7 @@ class RowContent extends StatelessWidget {
             if (builder != null) {
               accessories.addAll(builder(buildContext));
             }
+
             return accessories;
           },
           child: child,
@@ -289,12 +314,12 @@ class _RowEnterRegion extends StatefulWidget {
 }
 
 class _RowEnterRegionState extends State<_RowEnterRegion> {
-  late RegionStateNotifier _rowStateNotifier;
+  late final RegionStateNotifier _rowStateNotifier;
 
   @override
   void initState() {
-    _rowStateNotifier = RegionStateNotifier();
     super.initState();
+    _rowStateNotifier = RegionStateNotifier();
   }
 
   @override

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart

@@ -74,6 +74,7 @@ class GridCellBuilder {
           key: key,
         );
     }
+
     throw UnimplementedError;
   }
 }
@@ -83,7 +84,7 @@ class BlankCell extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Container();
+    return const SizedBox.shrink();
   }
 }
 

+ 6 - 9
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart

@@ -68,18 +68,15 @@ class CellContainer extends StatelessWidget {
     if (isFocus) {
       final borderSide = BorderSide(
         color: Theme.of(context).colorScheme.primary,
-        width: 1.0,
       );
+
       return BoxDecoration(border: Border.fromBorderSide(borderSide));
-    } else {
-      final borderSide = BorderSide(
-        color: Theme.of(context).dividerColor,
-        width: 1.0,
-      );
-      return BoxDecoration(
-        border: Border(right: borderSide, bottom: borderSide),
-      );
     }
+
+    final borderSide = BorderSide(color: Theme.of(context).dividerColor);
+    return BoxDecoration(
+      border: Border(right: borderSide, bottom: borderSide),
+    );
   }
 }
 

+ 5 - 5
frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart

@@ -1,10 +1,10 @@
 class HomeSizes {
-  static double get menuAddButtonHeight => 60;
-  static double get topBarHeight => 60;
-  static double get editPanelTopBarHeight => 60;
-  static double get editPanelWidth => 400;
+  static const double menuAddButtonHeight = 60;
+  static const double topBarHeight = 60;
+  static const double editPanelTopBarHeight = 60;
+  static const double editPanelWidth = 400;
 }
 
 class HomeInsets {
-  static double get topBarTitlePadding => 12;
+  static const double topBarTitlePadding = 12;
 }

+ 33 - 35
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart

@@ -170,23 +170,24 @@ class HomeStackManager {
     return MultiProvider(
       providers: [ChangeNotifierProvider.value(value: _notifier)],
       child: Consumer(
-        builder: (ctx, HomeStackNotifier notifier, child) {
+        builder: (_, HomeStackNotifier notifier, __) {
           return FadingIndexedStack(
             index: getIt<PluginSandbox>().indexOf(notifier.plugin.ty),
-            children:
-                getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
-              if (pluginType == notifier.plugin.ty) {
-                final pluginWidget = notifier.plugin.display
-                    .buildWidget(PluginContext(onDeleted: onDeleted));
-                if (pluginType == PluginType.editor) {
-                  return pluginWidget;
-                } else {
+            children: getIt<PluginSandbox>().supportPluginTypes.map(
+              (pluginType) {
+                if (pluginType == notifier.plugin.ty) {
+                  final pluginWidget = notifier.plugin.display
+                      .buildWidget(PluginContext(onDeleted: onDeleted));
+                  if (pluginType == PluginType.editor) {
+                    return pluginWidget;
+                  }
+
                   return pluginWidget.padding(horizontal: 40, vertical: 28);
                 }
-              } else {
+
                 return const BlankPage();
-              }
-            }).toList(),
+              },
+            ).toList(),
           );
         },
       ),
@@ -204,30 +205,27 @@ class HomeTopBar extends StatelessWidget {
     return Container(
       color: Theme.of(context).colorScheme.onSecondaryContainer,
       height: HomeSizes.topBarHeight,
-      child: Row(
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          HSpace(layout.menuSpacing),
-          const FlowyNavigation(),
-          const HSpace(16),
-          ChangeNotifierProvider.value(
-            value: Provider.of<HomeStackNotifier>(context, listen: false),
-            child: Consumer(
-              builder: (
-                BuildContext context,
-                HomeStackNotifier notifier,
-                Widget? child,
-              ) {
-                return notifier.plugin.display.rightBarItem ?? const SizedBox();
-              },
+      child: Padding(
+        padding: const EdgeInsets.symmetric(
+          horizontal: HomeInsets.topBarTitlePadding,
+        ),
+        child: Row(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            HSpace(layout.menuSpacing),
+            const FlowyNavigation(),
+            const HSpace(16),
+            ChangeNotifierProvider.value(
+              value: Provider.of<HomeStackNotifier>(context, listen: false),
+              child: Consumer(
+                builder: (_, HomeStackNotifier notifier, __) =>
+                    notifier.plugin.display.rightBarItem ??
+                    const SizedBox.shrink(),
+              ),
             ),
-          ) // _renderMoreButton(),
-        ],
-      )
-          .padding(
-            horizontal: HomeInsets.topBarTitlePadding,
-          )
-          .bottomBorder(color: Theme.of(context).dividerColor),
+          ],
+        ).bottomBorder(color: Theme.of(context).dividerColor),
+      ),
     );
   }
 }

+ 21 - 25
frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart

@@ -17,12 +17,13 @@ class ViewLeftBarItem extends StatefulWidget {
 class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
   final _controller = TextEditingController();
   final _focusNode = FocusNode();
-  late ViewService _viewService;
-  late ViewListener _viewListener;
+  late final ViewService _viewService;
+  late final ViewListener _viewListener;
   late ViewPB view;
 
   @override
   void initState() {
+    super.initState();
     view = widget.view;
     _viewService = ViewService();
     _focusNode.addListener(_handleFocusChanged);
@@ -39,7 +40,8 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
         );
       },
     );
-    super.initState();
+
+    _controller.text = view.name;
   }
 
   @override
@@ -53,30 +55,24 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
 
   @override
   Widget build(BuildContext context) {
-    _controller.text = view.name;
-
-    return IntrinsicWidth(
+    return GestureDetector(
       key: ValueKey(_controller.text),
-      child: GestureDetector(
-        onDoubleTap: () {
-          _controller.selection = TextSelection(
-            baseOffset: 0,
-            extentOffset: _controller.text.length,
-          );
-        },
-        child: TextField(
-          controller: _controller,
-          focusNode: _focusNode,
-          scrollPadding: EdgeInsets.zero,
-          decoration: const InputDecoration(
-            contentPadding: EdgeInsets.symmetric(vertical: 4.0),
-            border: InputBorder.none,
-            isDense: true,
-          ),
-          style: Theme.of(context).textTheme.bodyMedium,
-          // cursorColor: widget.cursorColor,
-          // obscureText: widget.enableObscure,
+      onDoubleTap: () {
+        _controller.selection = TextSelection(
+          baseOffset: 0,
+          extentOffset: _controller.text.length,
+        );
+      },
+      child: TextField(
+        controller: _controller,
+        focusNode: _focusNode,
+        scrollPadding: EdgeInsets.zero,
+        decoration: const InputDecoration(
+          contentPadding: EdgeInsets.symmetric(vertical: 4.0),
+          border: InputBorder.none,
+          isDense: true,
         ),
+        style: Theme.of(context).textTheme.bodyMedium,
       ),
     );
   }

+ 2 - 2
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart

@@ -3,7 +3,7 @@ export 'package:styled_widget/styled_widget.dart';
 
 extension FlowyStyledWidget on Widget {
   Widget bottomBorder({double width = 1.0, Color color = Colors.grey}) {
-    return Container(
+    return DecoratedBox(
       decoration: BoxDecoration(
         border: Border(
           bottom: BorderSide(width: width, color: color),
@@ -14,7 +14,7 @@ extension FlowyStyledWidget on Widget {
   }
 
   Widget topBorder({double width = 1.0, Color color = Colors.grey}) {
-    return Container(
+    return DecoratedBox(
       decoration: BoxDecoration(
         border: Border(
           top: BorderSide(width: width, color: color),

+ 15 - 4
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart

@@ -16,6 +16,7 @@ class FlowyIconButton extends StatelessWidget {
   final EdgeInsets iconPadding;
   final BorderRadius? radius;
   final String? tooltipText;
+  final InlineSpan? richTooltipText;
   final bool preferBelow;
 
   const FlowyIconButton({
@@ -29,15 +30,22 @@ class FlowyIconButton extends StatelessWidget {
     this.iconPadding = EdgeInsets.zero,
     this.radius,
     this.tooltipText,
+    this.richTooltipText,
     this.preferBelow = true,
     required this.icon,
-  }) : super(key: key);
+  })  : assert((richTooltipText != null && tooltipText == null) ||
+            (richTooltipText == null && tooltipText != null) ||
+            (richTooltipText == null && tooltipText == null)),
+        super(key: key);
 
   @override
   Widget build(BuildContext context) {
     Widget child = icon;
     final size = Size(width, height ?? width);
 
+    final tooltipMessage =
+        tooltipText == null && richTooltipText == null ? '' : tooltipText;
+
     assert(size.width > iconPadding.horizontal);
     assert(size.height > iconPadding.vertical);
 
@@ -46,11 +54,14 @@ class FlowyIconButton extends StatelessWidget {
     final childSize = Size(childWidth, childWidth);
 
     return ConstrainedBox(
-      constraints:
-          BoxConstraints.tightFor(width: size.width, height: size.height),
+      constraints: BoxConstraints.tightFor(
+        width: size.width,
+        height: size.height,
+      ),
       child: Tooltip(
         preferBelow: preferBelow,
-        message: tooltipText ?? '',
+        message: tooltipMessage,
+        richMessage: richTooltipText,
         showDuration: Duration.zero,
         child: RawMaterialButton(
           visualDensity: VisualDensity.compact,

+ 31 - 0
frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -13,9 +13,11 @@ void main() {
 
   group('Edit Grid:', () {
     late GridTestContext context;
+
     setUp(() async {
       context = await gridTest.createTestGrid();
     });
+
     // The initial number of rows is 3 for each grid.
     blocTest<GridBloc, GridState>(
       "create a row",
@@ -54,5 +56,34 @@ void main() {
         );
       },
     );
+
+    String? firstId;
+    String? secondId;
+    String? thirdId;
+
+    blocTest(
+      'reorder rows',
+      build: () => GridBloc(
+        view: context.gridView,
+        databaseController: DatabaseController(
+          view: context.gridView,
+          layoutType: LayoutTypePB.Grid,
+        ),
+      )..add(const GridEvent.initial()),
+      act: (bloc) async {
+        await gridResponseFuture();
+
+        firstId = bloc.state.rowInfos[0].rowPB.id;
+        secondId = bloc.state.rowInfos[1].rowPB.id;
+        thirdId = bloc.state.rowInfos[2].rowPB.id;
+
+        bloc.add(const GridEvent.moveRow(0, 2));
+      },
+      verify: (bloc) {
+        expect(secondId, bloc.state.rowInfos[0].rowPB.id);
+        expect(thirdId, bloc.state.rowInfos[1].rowPB.id);
+        expect(firstId, bloc.state.rowInfos[2].rowPB.id);
+      },
+    );
   });
 }