From f8f0599462ff0cca8b08dabd47f450147f3beecd Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Tue, 16 May 2023 14:20:05 +0100 Subject: [PATCH] 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 --- .../assets/translations/en.json | 1 + .../application/database_controller.dart | 14 +- .../application/database_view_service.dart | 14 +- .../application/row/row_cache.dart | 2 +- .../board/application/board_bloc.dart | 4 +- .../grid/application/grid_bloc.dart | 12 ++ .../grid/presentation/grid_page.dart | 179 ++++++++++-------- .../grid/presentation/widgets/row/row.dart | 89 +++++---- .../widgets/row/cell_builder.dart | 3 +- .../widgets/row/cells/cell_container.dart | 15 +- .../presentation/home/home_sizes.dart | 10 +- .../presentation/home/home_stack.dart | 68 ++++--- .../presentation/widgets/left_bar_item.dart | 46 ++--- .../lib/style_widget/extension.dart | 4 +- .../lib/style_widget/icon_button.dart | 19 +- .../bloc_test/grid_test/grid_bloc_test.dart | 31 +++ 16 files changed, 315 insertions(+), 196 deletions(-) diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index f394542eee..29aca3d611 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/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" }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 2ceca90de7..3755ebc343 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -175,18 +175,28 @@ class DatabaseController { ); } - Future> moveRow({ + Future> moveGroupRow({ required RowPB fromRow, required String groupId, RowPB? toRow, }) { - return _databaseViewBackendSvc.moveRow( + return _databaseViewBackendSvc.moveGroupRow( fromRowId: fromRow.id, toGroupId: groupId, toRowId: toRow?.id, ); } + Future> moveRow({ + required RowPB fromRow, + required RowPB toRow, + }) { + return _databaseViewBackendSvc.moveRow( + fromRowId: fromRow.id, + toRowId: toRow.id, + ); + } + Future> moveGroup({ required String fromGroupId, required String toGroupId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 7bb09dea50..7b6ae19b4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -44,7 +44,7 @@ class DatabaseViewBackendService { return DatabaseEventCreateRow(payload).send(); } - Future> moveRow({ + Future> moveGroupRow({ required String fromRowId, required String toGroupId, String? toRowId, @@ -61,6 +61,18 @@ class DatabaseViewBackendService { return DatabaseEventMoveGroupRow(payload).send(); } + Future> moveRow({ + required String fromRowId, + required String toRowId, + }) { + var payload = MoveRowPayloadPB.create() + ..viewId = viewId + ..fromRowId = fromRowId + ..toRowId = toRowId; + + return DatabaseEventMoveRow(payload).send(); + } + Future> moveGroup({ required String fromGroupId, required String toGroupId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 2bea00fc83..83ed4736ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/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(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 5c2fa3edd1..38bd02a2a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -53,7 +53,7 @@ class BoardBloc extends Bloc { 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 { 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, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 3ea6e03cf7..c3ddb337ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -35,6 +35,17 @@ class GridBloc extends Bloc { ); await rowService.deleteRow(rowInfo.rowPB.id); }, + moveRow: (int from, int to) { + final List 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 rows, RowsChangedReason listState, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 95f38e4ffd..e67acc9bd4 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -91,21 +91,6 @@ class _GridPageState extends State { ), ); } - - @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 { 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( - 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().state; + final sortState = context.watch().state; + + return BlocConsumer( + 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().state.rowInfos.length, - itemBuilder: - (BuildContext context, int index, Animation animation) { - final rowInfos = context.read().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().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() + .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 animation, - ) { + RowInfo rowInfo, { + int? index, + bool isSortEnabled = false, + bool isFilterEnabled = false, + Animation? animation, + }) { final rowCache = context.read().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().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( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index 52ba6fccb5..5fd0169c39 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/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 createState() => _GridRowState(); } class _GridRowState extends State { - 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 { return Row( children: [ - const _RowLeading(), + _RowLeading( + index: widget.index, + isDraggable: widget.isDraggable, + ), content, - const _RowTrailing(), ], ); }, @@ -89,19 +96,25 @@ class _GridRowState extends State { } 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 diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index 203da7f26e..31cf238b1a 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/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(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart index 5fb04100b0..54d3d56a8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart +++ b/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), + ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index 299d3fe03c..bc5b340b92 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/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; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 1ed67d05e8..e806cd8f50 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/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().indexOf(notifier.plugin.ty), - children: - getIt().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().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(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(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), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart index c00f084c3b..4816eececd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -17,12 +17,13 @@ class ViewLeftBarItem extends StatefulWidget { class _ViewLeftBarItemState extends State { 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 { ); }, ); - super.initState(); + + _controller.text = view.name; } @override @@ -53,30 +55,24 @@ class _ViewLeftBarItemState extends State { @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, ), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart index c41d3fc750..04ee12d4f3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart +++ b/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), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index 0e124e06c1..c8d089799e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/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, diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart index 352646d913..d2f8cc72fe 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/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( "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); + }, + ); }); }