Browse Source

feat: revamp row detail page UI (#3328)

* feat: revamp row detail page UI

* chore: some minor details and fix tests

* fix: fix tests

* chore: remove unused field

* chore: code cleanup

* test: add reordering fields in row page tests

* chore: remove duplicate and delete row events

* chore: timestamp cell ui adjustment

* chore: remove unused code

* test: fix new integration tests
Richard Shiue 1 năm trước cách đây
mục cha
commit
1ca130d7de
23 tập tin đã thay đổi với 648 bổ sung487 xóa
  1. 4 1
      frontend/appflowy_flutter/integration_test/database_calendar_test.dart
  2. 37 2
      frontend/appflowy_flutter/integration_test/database_row_page_test.dart
  3. 43 1
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  4. 1 7
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  5. 1 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart
  6. 31 19
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  7. 6 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart
  8. 2 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart
  9. 22 57
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart
  10. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  11. 20 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart
  12. 28 11
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart
  13. 24 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell.dart
  14. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart
  15. 10 8
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart
  16. 7 4
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart
  17. 26 33
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart
  18. 30 113
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart
  19. 113 62
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart
  20. 20 114
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  21. 201 39
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart
  22. 5 0
      frontend/resources/flowy_icons/16x/details_horizontal.svg
  23. 13 0
      frontend/resources/flowy_icons/16x/emoji.svg

+ 4 - 1
frontend/appflowy_flutter/integration_test/database_calendar_test.dart

@@ -9,7 +9,7 @@ import 'util/util.dart';
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
-  group('calendar database view', () {
+  group('calendar', () {
     testWidgets('update calendar layout', (tester) async {
       await tester.initializeAppFlowy();
       await tester.tapGoButton();
@@ -116,6 +116,7 @@ void main() {
       tester.assertRowDetailPageOpened();
 
       // Duplicate the event
+      await tester.tapRowDetailPageRowActionButton();
       await tester.tapRowDetailPageDuplicateRowButton();
       await tester.dismissRowDetailPage();
 
@@ -125,6 +126,7 @@ void main() {
 
       // Delete an event
       await tester.openCalendarEvent(index: 1);
+      await tester.tapRowDetailPageRowActionButton();
       await tester.tapRowDetailPageDeleteRowButton();
 
       // Check that there is 1 event
@@ -155,6 +157,7 @@ void main() {
 
       // Delete the event
       await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
+      await tester.tapRowDetailPageRowActionButton();
       await tester.tapRowDetailPageDeleteRowButton();
 
       // Create a new event in today's calendar cell

+ 37 - 2
frontend/appflowy_flutter/integration_test/database_row_page_test.dart

@@ -135,7 +135,40 @@ void main() {
       }
     });
 
-    testWidgets('check document is exist in row detail page', (tester) async {
+    testWidgets('change order of fields and cells', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      // Assert that the first field in the row details page is the select
+      // option tyoe
+      tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
+
+      // Reorder first field in list
+      final gesture = await tester.hoverOnFieldInRowDetail(index: 0);
+      await tester.pumpAndSettle();
+      await tester.reorderFieldInRowDetail(offset: 30);
+
+      // Orders changed, now the checkbox is first
+      tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);
+      await gesture.removePointer();
+      await tester.pumpAndSettle();
+
+      // Reorder second field in list
+      await tester.hoverOnFieldInRowDetail(index: 1);
+      await tester.pumpAndSettle();
+      await tester.reorderFieldInRowDetail(offset: -30);
+
+      // First field is now back to select option
+      tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
+    });
+
+    testWidgets('check document exists in row detail page', (tester) async {
       await tester.initializeAppFlowy();
       await tester.tapGoButton();
 
@@ -149,7 +182,7 @@ void main() {
       await tester.assertDocumentExistInRowDetailPage();
     });
 
-    testWidgets('update the content of the document and re-open it',
+    testWidgets('update the contents of the document and re-open it',
         (tester) async {
       await tester.initializeAppFlowy();
       await tester.tapGoButton();
@@ -239,6 +272,7 @@ void main() {
       // Hover first row and then open the row page
       await tester.openFirstRowDetailPage();
 
+      await tester.tapRowDetailPageRowActionButton();
       await tester.tapRowDetailPageDeleteRowButton();
       await tester.tapEscButton();
 
@@ -255,6 +289,7 @@ void main() {
       // Hover first row and then open the row page
       await tester.openFirstRowDetailPage();
 
+      await tester.tapRowDetailPageRowActionButton();
       await tester.tapRowDetailPageDuplicateRowButton();
       await tester.tapEscButton();
 

+ 43 - 1
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -48,6 +48,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
 import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
 import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
@@ -485,7 +486,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
     expect(banner, findsOneWidget);
 
     await startGesture(
-      getTopLeft(banner),
+      getCenter(banner),
       kind: PointerDeviceKind.mouse,
     );
 
@@ -524,6 +525,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(deleteButton);
   }
 
+  Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
+    final fieldButtons = find.byType(FieldCellButton);
+    final button = find
+        .descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
+        .at(index);
+    return startGesture(
+      getCenter(button),
+      kind: PointerDeviceKind.mouse,
+    );
+  }
+
+  Future<void> reorderFieldInRowDetail({required double offset}) async {
+    final thumb = find
+        .byWidgetPredicate(
+          (widget) => widget is ReorderableDragStartListener && widget.enabled,
+        )
+        .first;
+    await drag(
+      thumb,
+      Offset(0, offset),
+      kind: PointerDeviceKind.mouse,
+    );
+    await pumpAndSettle();
+  }
+
   Future<void> scrollGridByOffset(Offset offset) async {
     await drag(find.byType(GridPage), offset);
     await pumpAndSettle();
@@ -601,6 +627,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(button);
   }
 
+  Future<void> tapRowDetailPageRowActionButton() async {
+    await tapButton(find.byType(RowActionButton));
+  }
+
   Future<void> tapRowDetailPageCreatePropertyButton() async {
     await tapButton(find.byType(CreateRowFieldButton));
   }
@@ -670,6 +700,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
     expect(field, findsOneWidget);
   }
 
+  void assertFirstFieldInRowDetailByType(FieldType fieldType) {
+    final firstField = find
+        .descendant(
+          of: find.byType(RowDetailPage),
+          matching: find.byType(FieldCellButton),
+        )
+        .first;
+
+    final widget = this.widget<FieldCellButton>(firstField);
+    expect(widget.field.fieldType, fieldType);
+  }
+
   Future<void> findFieldWithName(String name) async {
     final field = find.byWidgetPredicate(
       (widget) => widget is FieldCellButton && widget.field.name == name,

+ 1 - 7
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -395,13 +395,7 @@ class RowDataBuilder {
   }
 
   void insertDate(FieldInfo fieldInfo, DateTime date) {
-    assert(
-      [
-        FieldType.DateTime,
-        FieldType.LastEditedTime,
-        FieldType.CreatedTime,
-      ].contains(fieldInfo.fieldType),
-    );
+    assert(FieldType.DateTime == fieldInfo.fieldType);
     final timestamp = date.millisecondsSinceEpoch ~/ 1000;
     _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
   }

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

@@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
             await _listenRowMeteChanged();
           },
           didReceiveRowMeta: (RowMetaPB rowMeta) {
-            emit(
-              state.copyWith(
-                rowMeta: rowMeta,
-              ),
-            );
+            emit(state.copyWith(rowMeta: rowMeta));
           },
           setCover: (String coverURL) {
             _updateMeta(coverURL: coverURL);

+ 31 - 19
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -1,23 +1,20 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
 import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-import 'dart:async';
-import '../../../application/cell/cell_service.dart';
-import '../../../application/field/field_service.dart';
-import '../../../application/row/row_controller.dart';
+
 part 'row_detail_bloc.freezed.dart';
 
 class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
-  final RowBackendService rowService;
   final RowController rowController;
 
   RowDetailBloc({
     required this.rowController,
-  })  : rowService = RowBackendService(viewId: rowController.viewId),
-        super(RowDetailState.initial()) {
+  }) : super(RowDetailState.initial()) {
     on<RowDetailEvent>(
       (event, emit) async {
         await event.when(
@@ -58,14 +55,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
               (err) => Log.error(err),
             );
           },
-          deleteRow: (rowId) async {
-            await rowService.deleteRow(rowId);
-          },
-          duplicateRow: (String rowId, String? groupId) async {
-            await rowService.duplicateRow(
-              rowId: rowId,
-              groupId: groupId,
-            );
+          reorderField: (fieldId, fromIndex, toIndex) async {
+            await _reorderField(fieldId, fromIndex, toIndex, emit);
           },
         );
       },
@@ -94,6 +85,25 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
       fieldId: fieldId,
     );
   }
+
+  Future<void> _reorderField(
+    String fieldId,
+    int fromIndex,
+    int toIndex,
+    Emitter<RowDetailState> emit,
+  ) async {
+    final cells = List<DatabaseCellContext>.from(state.cells);
+    cells.insert(toIndex, cells.removeAt(fromIndex));
+    emit(state.copyWith(cells: cells));
+
+    final fieldService =
+        FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
+    final result = await fieldService.moveField(
+      fromIndex,
+      toIndex,
+    );
+    result.fold((l) {}, (err) => Log.error(err));
+  }
 }
 
 @freezed
@@ -102,9 +112,11 @@ class RowDetailEvent with _$RowDetailEvent {
   const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
   const factory RowDetailEvent.showField(String fieldId) = _ShowField;
   const factory RowDetailEvent.hideField(String fieldId) = _HideField;
-  const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
-  const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
-      _DuplicateRow;
+  const factory RowDetailEvent.reorderField(
+    String fieldId,
+    int fromIndex,
+    int toIndex,
+  ) = _ReorderField;
   const factory RowDetailEvent.didReceiveCellDatas(
     List<DatabaseCellContext> gridCells,
   ) = _DidReceiveCellDatas;

+ 6 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart

@@ -154,29 +154,33 @@ class FieldCellButton extends StatelessWidget {
   final FieldPB field;
   final int? maxLines;
   final BorderRadius? radius;
+  final EdgeInsets? margin;
   const FieldCellButton({
     required this.field,
     required this.onTap,
     this.maxLines = 1,
     this.radius = BorderRadius.zero,
+    this.margin,
     Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return FlowyButton(
-      hoverColor: AFThemeExtension.of(context).greyHover,
+      hoverColor: AFThemeExtension.of(context).lightGreyHover,
       onTap: onTap,
       leftIcon: FlowySvg(
         field.fieldType.icon(),
+        color: Theme.of(context).iconTheme.color,
       ),
       radius: radius,
       text: FlowyText.medium(
         field.name,
         maxLines: maxLines,
         overflow: TextOverflow.ellipsis,
+        color: AFThemeExtension.of(context).textColor,
       ),
-      margin: GridSize.cellContentInsets,
+      margin: margin ?? GridSize.cellContentInsets,
     );
   }
 }

+ 2 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart

@@ -40,8 +40,7 @@ class RowActions extends StatelessWidget {
               .map((action) => _ActionCell(action: action))
               .toList();
 
-          //
-          final list = ListView.separated(
+          return ListView.separated(
             shrinkWrap: true,
             controller: ScrollController(),
             itemCount: cells.length,
@@ -53,7 +52,6 @@ class RowActions extends StatelessWidget {
               return cells[index];
             },
           );
-          return list;
         },
       ),
     );
@@ -70,6 +68,7 @@ class _ActionCell extends StatelessWidget {
       height: GridSize.popoverItemHeight,
       child: FlowyButton(
         hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        useIntrinsicWidth: true,
         text: FlowyText.medium(
           action.title(),
           color: action.enable()

+ 22 - 57
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart

@@ -1,9 +1,9 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme_extension.dart';
 
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -91,50 +91,31 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
 
 class AccessoryHover extends StatefulWidget {
   final CellAccessory child;
-  final EdgeInsets contentPadding;
-  const AccessoryHover({
-    required this.child,
-    this.contentPadding = EdgeInsets.zero,
-    Key? key,
-  }) : super(key: key);
+  const AccessoryHover({required this.child, super.key});
 
   @override
   State<AccessoryHover> createState() => _AccessoryHoverState();
 }
 
 class _AccessoryHoverState extends State<AccessoryHover> {
-  late AccessoryHoverState _hoverState;
-  VoidCallback? _listenerFn;
-
-  @override
-  void initState() {
-    _hoverState = AccessoryHoverState();
-    _listenerFn = () =>
-        _hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
-    widget.child.onAccessoryHover?.addListener(_listenerFn!);
-
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    _hoverState.dispose();
-
-    if (_listenerFn != null) {
-      widget.child.onAccessoryHover?.removeListener(_listenerFn!);
-      _listenerFn = null;
-    }
-    super.dispose();
-  }
+  bool _isHover = false;
 
   @override
   Widget build(BuildContext context) {
     final List<Widget> children = [
-      Padding(padding: widget.contentPadding, child: widget.child),
+      DecoratedBox(
+        decoration: BoxDecoration(
+          color: _isHover
+              ? AFThemeExtension.of(context).lightGreyHover
+              : Colors.transparent,
+          borderRadius: Corners.s6Border,
+        ),
+        child: widget.child,
+      ),
     ];
 
     final accessoryBuilder = widget.child.accessoryBuilder;
-    if (accessoryBuilder != null) {
+    if (accessoryBuilder != null && _isHover) {
       final accessories = accessoryBuilder(
         (GridCellAccessoryBuildContext(
           anchorContext: context,
@@ -149,36 +130,20 @@ class _AccessoryHoverState extends State<AccessoryHover> {
       );
     }
 
-    return ChangeNotifierProvider.value(
-      value: _hoverState,
-      child: MouseRegion(
-        cursor: SystemMouseCursors.click,
-        opaque: false,
-        onEnter: (p) => setState(() => _hoverState.onHover = true),
-        onExit: (p) => setState(() => _hoverState.onHover = false),
-        child: Stack(
-          fit: StackFit.loose,
-          alignment: AlignmentDirectional.center,
-          children: children,
-        ),
+    return MouseRegion(
+      cursor: SystemMouseCursors.click,
+      opaque: false,
+      onEnter: (p) => setState(() => _isHover = true),
+      onExit: (p) => setState(() => _isHover = false),
+      child: Stack(
+        fit: StackFit.loose,
+        alignment: AlignmentDirectional.center,
+        children: children,
       ),
     );
   }
 }
 
-class AccessoryHoverState extends ChangeNotifier {
-  bool _onHover = false;
-
-  set onHover(bool value) {
-    if (_onHover != value) {
-      _onHover = value;
-      notifyListeners();
-    }
-  }
-
-  bool get onHover => _onHover;
-}
-
 class CellAccessoryContainer extends StatelessWidget {
   final List<GridCellAccessoryBuilder> accessories;
   const CellAccessoryContainer({required this.accessories, Key? key})

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

@@ -35,6 +35,7 @@ class GridCellBuilder {
       case FieldType.Checkbox:
         return GridCheckboxCell(
           cellControllerBuilder: cellControllerBuilder,
+          style: style,
           key: key,
         );
       case FieldType.DateTime:
@@ -71,6 +72,7 @@ class GridCellBuilder {
       case FieldType.Number:
         return GridNumberCell(
           cellControllerBuilder: cellControllerBuilder,
+          style: style,
           key: key,
         );
       case FieldType.RichText:

+ 20 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart

@@ -9,12 +9,29 @@ import 'checkbox_cell_bloc.dart';
 import '../../../../grid/presentation/layout/sizes.dart';
 import '../../cell_builder.dart';
 
+class GridCheckboxCellStyle extends GridCellStyle {
+  EdgeInsets? cellPadding;
+
+  GridCheckboxCellStyle({
+    this.cellPadding,
+  });
+}
+
 class GridCheckboxCell extends GridCellWidget {
   final CellControllerBuilder cellControllerBuilder;
+  late final GridCheckboxCellStyle cellStyle;
+
   GridCheckboxCell({
     required this.cellControllerBuilder,
+    GridCellStyle? style,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key) {
+    if (style != null) {
+      cellStyle = (style as GridCheckboxCellStyle);
+    } else {
+      cellStyle = GridCheckboxCellStyle();
+    }
+  }
 
   @override
   GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
@@ -46,7 +63,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
           return Align(
             alignment: Alignment.centerLeft,
             child: Padding(
-              padding: GridSize.cellContentInsets,
+              padding:
+                  widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
               child: FlowyIconButton(
                 hoverColor: Colors.transparent,
                 onPressed: () => context

+ 28 - 11
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart

@@ -1,7 +1,8 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import '../../../../grid/presentation/layout/sizes.dart';
@@ -10,9 +11,15 @@ import 'date_cell_bloc.dart';
 import 'date_editor.dart';
 
 class DateCellStyle extends GridCellStyle {
+  String? placeholder;
   Alignment alignment;
+  EdgeInsets? cellPadding;
 
-  DateCellStyle({this.alignment = Alignment.center});
+  DateCellStyle({
+    this.placeholder,
+    this.alignment = Alignment.center,
+    this.cellPadding,
+  });
 }
 
 abstract class GridCellDelegate {
@@ -71,7 +78,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
             margin: EdgeInsets.zero,
             child: GridDateCellText(
               dateStr: state.dateStr,
+              placeholder: widget.cellStyle?.placeholder ?? "",
               alignment: alignment,
+              cellPadding:
+                  widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
             ),
             popupBuilder: (BuildContext popoverContent) {
               return DateCellEditor(
@@ -107,24 +117,31 @@ class _DateCellState extends GridCellState<GridDateCell> {
 
 class GridDateCellText extends StatelessWidget {
   final String dateStr;
+  final String placeholder;
   final Alignment alignment;
+  final EdgeInsets cellPadding;
   const GridDateCellText({
     required this.dateStr,
+    required this.placeholder,
     required this.alignment,
+    required this.cellPadding,
     super.key,
   });
 
   @override
   Widget build(BuildContext context) {
-    return SizedBox.expand(
-      child: Align(
-        alignment: alignment,
-        child: Padding(
-          padding: GridSize.cellContentInsets,
-          child: FlowyText.medium(
-            dateStr,
-            maxLines: null,
-          ),
+    final isPlaceholder = dateStr.isEmpty;
+    final text = isPlaceholder ? placeholder : dateStr;
+    return Align(
+      alignment: alignment,
+      child: Padding(
+        padding: cellPadding,
+        child: FlowyText.medium(
+          text,
+          color: isPlaceholder
+              ? Theme.of(context).hintColor
+              : AFThemeExtension.of(context).textColor,
+          maxLines: null,
         ),
       ),
     );

+ 24 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell.dart

@@ -7,13 +7,33 @@ import 'number_cell_bloc.dart';
 import '../../../../grid/presentation/layout/sizes.dart';
 import '../../cell_builder.dart';
 
+class GridNumberCellStyle extends GridCellStyle {
+  String? placeholder;
+  TextStyle? textStyle;
+  EdgeInsets? cellPadding;
+
+  GridNumberCellStyle({
+    this.placeholder,
+    this.textStyle,
+    this.cellPadding,
+  });
+}
+
 class GridNumberCell extends GridCellWidget {
   final CellControllerBuilder cellControllerBuilder;
+  late final GridNumberCellStyle cellStyle;
 
   GridNumberCell({
     required this.cellControllerBuilder,
-    Key? key,
-  }) : super(key: key);
+    required GridCellStyle? style,
+    super.key,
+  }) {
+    if (style != null) {
+      cellStyle = (style as GridNumberCellStyle);
+    } else {
+      cellStyle = GridNumberCellStyle();
+    }
+  }
 
   @override
   GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
@@ -57,9 +77,10 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
             maxLines: null,
             style: Theme.of(context).textTheme.bodyMedium,
             textInputAction: TextInputAction.done,
-            decoration: const InputDecoration(
+            decoration: InputDecoration(
               contentPadding: EdgeInsets.zero,
               border: InputBorder.none,
+              hintText: widget.cellStyle.placeholder,
               isDense: true,
             ),
           ),

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart

@@ -93,7 +93,7 @@ class SelectOptionTag extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     EdgeInsets padding =
-        const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0);
+        const EdgeInsets.symmetric(vertical: 1.5, horizontal: 8.0);
     if (onRemove != null) {
       padding = padding.copyWith(right: 2.0);
     }
@@ -110,6 +110,7 @@ class SelectOptionTag extends StatelessWidget {
           Flexible(
             child: FlowyText.medium(
               name,
+              fontSize: FontSizes.s11,
               overflow: TextOverflow.ellipsis,
               color: AFThemeExtension.of(context).textColor,
             ),

+ 10 - 8
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart

@@ -13,9 +13,11 @@ import 'select_option_editor.dart';
 
 class SelectOptionCellStyle extends GridCellStyle {
   String placeholder;
+  EdgeInsets? cellPadding;
 
   SelectOptionCellStyle({
     required this.placeholder,
+    this.cellPadding,
   });
 }
 
@@ -170,10 +172,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
     final Widget child = _buildOptions(context);
 
     final constraints = BoxConstraints.loose(
-      Size(
-        SelectOptionCellEditor.editorPanelWidth,
-        300,
-      ),
+      Size(SelectOptionCellEditor.editorPanelWidth, 300),
     );
     return AppFlowyPopover(
       controller: widget.popoverController,
@@ -191,7 +190,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
       },
       onClose: () => widget.onCellEditing.value = false,
       child: Padding(
-        padding: GridSize.cellContentInsets,
+        padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
         child: child,
       ),
     );
@@ -200,9 +199,12 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
   Widget _buildOptions(BuildContext context) {
     final Widget child;
     if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
-      child = FlowyText.medium(
-        widget.cellStyle!.placeholder,
-        color: Theme.of(context).hintColor,
+      child = Padding(
+        padding: const EdgeInsets.symmetric(vertical: 1),
+        child: FlowyText.medium(
+          widget.cellStyle!.placeholder,
+          color: Theme.of(context).hintColor,
+        ),
       );
     } else {
       final children = widget.selectOptions.map(

+ 7 - 4
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart

@@ -14,6 +14,7 @@ class GridTextCellStyle extends GridCellStyle {
   double emojiFontSize;
   double emojiHPadding;
   bool showEmoji;
+  EdgeInsets? cellPadding;
 
   GridTextCellStyle({
     this.placeholder,
@@ -22,6 +23,7 @@ class GridTextCellStyle extends GridCellStyle {
     this.showEmoji = true,
     this.emojiFontSize = 16,
     this.emojiHPadding = 0,
+    this.cellPadding,
   });
 }
 
@@ -72,10 +74,11 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
           }
         },
         child: Padding(
-          padding: EdgeInsets.only(
-            left: GridSize.cellContentInsets.left,
-            right: GridSize.cellContentInsets.right,
-          ),
+          padding: widget.cellStyle.cellPadding ??
+              EdgeInsets.only(
+                left: GridSize.cellContentInsets.left,
+                right: GridSize.cellContentInsets.right,
+              ),
           child: Row(
             children: [
               if (widget.cellStyle.showEmoji)

+ 26 - 33
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart

@@ -3,14 +3,21 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da
 import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class TimestampCellStyle extends GridCellStyle {
+  String? placeholder;
   Alignment alignment;
+  EdgeInsets? cellPadding;
 
-  TimestampCellStyle({this.alignment = Alignment.center});
+  TimestampCellStyle({
+    this.placeholder,
+    this.alignment = Alignment.center,
+    this.cellPadding,
+  });
 }
 
 class GridTimestampCell extends GridCellWidget {
@@ -51,16 +58,28 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
 
   @override
   Widget build(BuildContext context) {
-    final alignment = widget.cellStyle != null
-        ? widget.cellStyle!.alignment
-        : Alignment.centerLeft;
+    final alignment = widget.cellStyle?.alignment ?? Alignment.centerLeft;
+    final placeholder = widget.cellStyle?.placeholder ?? "";
+    final padding = widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets;
+
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
         builder: (context, state) {
-          return GridTimestampCellText(
-            dateStr: state.dateStr,
+          final isEmpty = state.dateStr.isEmpty;
+          final text = isEmpty ? placeholder : state.dateStr;
+          return Align(
             alignment: alignment,
+            child: Padding(
+              padding: padding,
+              child: FlowyText.medium(
+                text,
+                color: isEmpty
+                    ? Theme.of(context).hintColor
+                    : AFThemeExtension.of(context).textColor,
+                maxLines: null,
+              ),
+            ),
           );
         },
       ),
@@ -81,29 +100,3 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
     return;
   }
 }
-
-class GridTimestampCellText extends StatelessWidget {
-  final String dateStr;
-  final Alignment alignment;
-  const GridTimestampCellText({
-    required this.dateStr,
-    required this.alignment,
-    super.key,
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox.expand(
-      child: Align(
-        alignment: alignment,
-        child: Padding(
-          padding: GridSize.cellContentInsets,
-          child: FlowyText.medium(
-            dateStr,
-            maxLines: null,
-          ),
-        ),
-      ),
-    );
-  }
-}

+ 30 - 113
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart

@@ -1,18 +1,10 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
-import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
-import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
-import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
-import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 
-import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -20,36 +12,39 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 class RowActionList extends StatelessWidget {
   final RowController rowController;
   const RowActionList({
-    required String viewId,
     required this.rowController,
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
   @override
   Widget build(BuildContext context) {
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        Padding(
-          padding: const EdgeInsets.only(left: 10),
-          child: FlowyText(LocaleKeys.grid_row_action.tr()),
-        ),
-        const VSpace(15),
-        RowDetailPageDeleteButton(rowId: rowController.rowId),
-        RowDetailPageDuplicateButton(
-          rowId: rowController.rowId,
-          groupId: rowController.groupId,
+    return BlocProvider<RowActionSheetBloc>(
+      create: (context) => RowActionSheetBloc(
+        viewId: rowController.viewId,
+        rowId: rowController.rowId,
+        groupId: rowController.groupId,
+      ),
+      child: IntrinsicWidth(
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            RowDetailPageDuplicateButton(
+              rowId: rowController.rowId,
+              groupId: rowController.groupId,
+            ),
+            const VSpace(4.0),
+            RowDetailPageDeleteButton(rowId: rowController.rowId),
+          ],
         ),
-      ],
+      ),
     );
   }
 }
 
 class RowDetailPageDeleteButton extends StatelessWidget {
   final String rowId;
-  const RowDetailPageDeleteButton({required this.rowId, Key? key})
-      : super(key: key);
+  const RowDetailPageDeleteButton({required this.rowId, super.key});
 
   @override
   Widget build(BuildContext context) {
@@ -59,7 +54,9 @@ class RowDetailPageDeleteButton extends StatelessWidget {
         text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
         leftIcon: const FlowySvg(FlowySvgs.trash_m),
         onTap: () {
-          context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
+          context
+              .read<RowActionSheetBloc>()
+              .add(const RowActionSheetEvent.deleteRow());
           FlowyOverlay.pop(context);
         },
       ),
@@ -73,8 +70,8 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
   const RowDetailPageDuplicateButton({
     required this.rowId,
     this.groupId,
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -85,91 +82,11 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
         leftIcon: const FlowySvg(FlowySvgs.copy_s),
         onTap: () {
           context
-              .read<RowDetailBloc>()
-              .add(RowDetailEvent.duplicateRow(rowId, groupId));
+              .read<RowActionSheetBloc>()
+              .add(const RowActionSheetEvent.duplicateRow());
           FlowyOverlay.pop(context);
         },
       ),
     );
   }
 }
-
-class CreateRowFieldButton extends StatefulWidget {
-  final String viewId;
-
-  const CreateRowFieldButton({
-    required this.viewId,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
-}
-
-class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
-  late PopoverController popoverController;
-  late TypeOptionPB typeOption;
-
-  @override
-  void initState() {
-    popoverController = PopoverController();
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return AppFlowyPopover(
-      constraints: BoxConstraints.loose(const Size(240, 200)),
-      controller: popoverController,
-      direction: PopoverDirection.topWithLeftAligned,
-      triggerActions: PopoverTriggerFlags.none,
-      margin: EdgeInsets.zero,
-      child: SizedBox(
-        height: 40,
-        child: FlowyButton(
-          text: FlowyText.medium(
-            LocaleKeys.grid_field_newProperty.tr(),
-            color: AFThemeExtension.of(context).textColor,
-          ),
-          hoverColor: AFThemeExtension.of(context).lightGreyHover,
-          onTap: () async {
-            final result = await TypeOptionBackendService.createFieldTypeOption(
-              viewId: widget.viewId,
-            );
-            result.fold(
-              (l) {
-                typeOption = l;
-                popoverController.show();
-              },
-              (r) => Log.error("Failed to create field type option: $r"),
-            );
-          },
-          leftIcon: FlowySvg(
-            FlowySvgs.add_m,
-            color: AFThemeExtension.of(context).textColor,
-          ),
-        ),
-      ),
-      popupBuilder: (BuildContext popOverContext) {
-        return FieldEditor(
-          viewId: widget.viewId,
-          typeOptionLoader: FieldTypeOptionLoader(
-            viewId: widget.viewId,
-            field: typeOption.field_2,
-          ),
-          onDeleted: (fieldId) {
-            popoverController.close();
-            NavigatorAlertDialog(
-              title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
-              confirm: () {
-                context
-                    .read<RowDetailBloc>()
-                    .add(RowDetailEvent.deleteField(fieldId));
-              },
-            ).show(context);
-          },
-        );
-      },
-    );
-  }
-}

+ 113 - 62
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart

@@ -1,23 +1,27 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-typedef RowBannerCellBuilder = Widget Function(String fieldId);
+import 'cell_builder.dart';
+import 'cells/cells.dart';
 
 class RowBanner extends StatefulWidget {
-  final String viewId;
-  final RowMetaPB rowMeta;
-  final RowBannerCellBuilder cellBuilder;
+  final RowController rowController;
+  final GridCellBuilder cellBuilder;
+
   const RowBanner({
-    required this.viewId,
-    required this.rowMeta,
+    required this.rowController,
     required this.cellBuilder,
     super.key,
   });
@@ -34,25 +38,39 @@ class _RowBannerState extends State<RowBanner> {
   Widget build(BuildContext context) {
     return BlocProvider<RowBannerBloc>(
       create: (context) => RowBannerBloc(
-        viewId: widget.viewId,
-        rowMeta: widget.rowMeta,
+        viewId: widget.rowController.viewId,
+        rowMeta: widget.rowController.rowMeta,
       )..add(const RowBannerEvent.initial()),
       child: MouseRegion(
         onEnter: (event) => _isHovering.value = true,
         onExit: (event) => _isHovering.value = false,
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
+        child: Stack(
           children: [
-            SizedBox(
-              height: 30,
-              child: _BannerAction(
-                isHovering: _isHovering,
-                popoverController: popoverController,
+            Padding(
+              padding: const EdgeInsets.fromLTRB(60, 34, 60, 0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  SizedBox(
+                    height: 30,
+                    child: _BannerAction(
+                      isHovering: _isHovering,
+                      popoverController: popoverController,
+                    ),
+                  ),
+                  const HSpace(4),
+                  _BannerTitle(
+                    cellBuilder: widget.cellBuilder,
+                    popoverController: popoverController,
+                    rowController: widget.rowController,
+                  ),
+                ],
               ),
             ),
-            _BannerTitle(
-              cellBuilder: widget.cellBuilder,
-              popoverController: popoverController,
+            Positioned(
+              top: 12,
+              right: 12,
+              child: RowActionButton(rowController: widget.rowController),
             ),
           ],
         ),
@@ -74,50 +92,54 @@ class _BannerAction extends StatelessWidget {
     return ValueListenableBuilder(
       valueListenable: isHovering,
       builder: (BuildContext context, bool value, Widget? child) {
-        if (value) {
-          return BlocBuilder<RowBannerBloc, RowBannerState>(
-            builder: (context, state) {
-              final children = <Widget>[];
-              final rowMeta = state.rowMeta;
-              if (rowMeta.icon.isEmpty) {
-                children.add(
-                  EmojiPickerButton(
-                    showEmojiPicker: () => popoverController.show(),
-                  ),
-                );
-              } else {
-                children.add(
-                  RemoveEmojiButton(
-                    onRemoved: () {
-                      context
-                          .read<RowBannerBloc>()
-                          .add(const RowBannerEvent.setIcon(''));
-                    },
-                  ),
-                );
-              }
-              return Row(
-                mainAxisSize: MainAxisSize.min,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: children,
-              );
-            },
-          );
-        } else {
+        if (!value) {
           return const SizedBox(height: _kBannerActionHeight);
         }
+
+        return BlocBuilder<RowBannerBloc, RowBannerState>(
+          builder: (context, state) {
+            final children = <Widget>[];
+            final rowMeta = state.rowMeta;
+            if (rowMeta.icon.isEmpty) {
+              children.add(
+                EmojiPickerButton(
+                  showEmojiPicker: () => popoverController.show(),
+                ),
+              );
+            } else {
+              children.add(
+                RemoveEmojiButton(
+                  onRemoved: () {
+                    context
+                        .read<RowBannerBloc>()
+                        .add(const RowBannerEvent.setIcon(''));
+                  },
+                ),
+              );
+            }
+            return Row(
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: children,
+            );
+          },
+        );
       },
     );
   }
 }
 
 class _BannerTitle extends StatefulWidget {
-  final RowBannerCellBuilder cellBuilder;
+  final GridCellBuilder cellBuilder;
   final PopoverController popoverController;
+  final RowController rowController;
+
   const _BannerTitle({
     required this.cellBuilder,
     required this.popoverController,
-  });
+    required this.rowController,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<_BannerTitle> createState() => _BannerTitleState();
@@ -139,10 +161,24 @@ class _BannerTitleState extends State<_BannerTitle> {
           );
         }
 
+        children.add(const HSpace(4));
+
         if (state.primaryField != null) {
+          final style = GridTextCellStyle(
+            placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
+            textStyle: Theme.of(context).textTheme.titleLarge,
+            showEmoji: false,
+            autofocus: true,
+            cellPadding: EdgeInsets.zero,
+          );
+          final cellContext = DatabaseCellContext(
+            viewId: widget.rowController.viewId,
+            rowMeta: widget.rowController.rowMeta,
+            fieldInfo: FieldInfo.initial(state.primaryField!),
+          );
           children.add(
             Expanded(
-              child: widget.cellBuilder(state.primaryField!.id),
+              child: widget.cellBuilder.build(cellContext, style: style),
             ),
           );
         }
@@ -211,16 +247,14 @@ class _EmojiPickerButtonState extends State<EmojiPickerButton> {
   Widget build(BuildContext context) {
     return SizedBox(
       height: 26,
-      width: 160,
       child: FlowyButton(
+        useIntrinsicWidth: true,
         text: FlowyText.medium(
           LocaleKeys.document_plugins_cover_addIcon.tr(),
         ),
-        leftIcon: const Icon(
-          Icons.emoji_emotions,
-          size: 16,
-        ),
+        leftIcon: const FlowySvg(FlowySvgs.emoji_s),
         onTap: widget.showEmojiPicker,
+        margin: const EdgeInsets.all(4),
       ),
     );
   }
@@ -239,16 +273,14 @@ class RemoveEmojiButton extends StatelessWidget {
   Widget build(BuildContext context) {
     return SizedBox(
       height: 26,
-      width: 160,
       child: FlowyButton(
+        useIntrinsicWidth: true,
         text: FlowyText.medium(
           LocaleKeys.document_plugins_cover_removeIcon.tr(),
         ),
-        leftIcon: const Icon(
-          Icons.emoji_emotions,
-          size: 16,
-        ),
+        leftIcon: const FlowySvg(FlowySvgs.emoji_s),
         onTap: onRemoved,
+        margin: const EdgeInsets.all(4),
       ),
     );
   }
@@ -263,3 +295,22 @@ Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
     ),
   );
 }
+
+class RowActionButton extends StatelessWidget {
+  final RowController rowController;
+  const RowActionButton({super.key, required this.rowController});
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      direction: PopoverDirection.bottomWithLeftAligned,
+      popupBuilder: (context) => RowActionList(rowController: rowController),
+      child: FlowyIconButton(
+        width: 20,
+        height: 20,
+        icon: const FlowySvg(FlowySvgs.details_horizontal_s),
+        iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
+      ),
+    );
+  }
+}

+ 20 - 114
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -1,18 +1,12 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
-import 'package:collection/collection.dart';
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'cell_builder.dart';
-import 'cells/text_cell/text_cell.dart';
-import 'row_action.dart';
 import 'row_banner.dart';
 import 'row_property.dart';
 
@@ -47,17 +41,29 @@ class _RowDetailPageState extends State<RowDetailPage> {
   Widget build(BuildContext context) {
     return FlowyDialog(
       child: BlocProvider(
-        create: (context) {
-          return RowDetailBloc(rowController: widget.rowController)
-            ..add(const RowDetailEvent.initial());
-        },
+        create: (context) => RowDetailBloc(rowController: widget.rowController)
+          ..add(const RowDetailEvent.initial()),
         child: ListView(
           controller: scrollController,
           children: [
-            _rowBanner(),
-            IntrinsicHeight(child: _responsiveRowInfo()),
-            const Divider(height: 1.0),
-            const VSpace(10),
+            RowBanner(
+              rowController: widget.rowController,
+              cellBuilder: widget.cellBuilder,
+            ),
+            const VSpace(16),
+            Padding(
+              padding: const EdgeInsets.only(left: 40, right: 60),
+              child: RowPropertyList(
+                cellBuilder: widget.cellBuilder,
+                viewId: widget.rowController.viewId,
+              ),
+            ),
+            const VSpace(20),
+            const Padding(
+              padding: EdgeInsets.symmetric(horizontal: 60),
+              child: Divider(height: 1.0),
+            ),
+            const VSpace(20),
             RowDocument(
               viewId: widget.rowController.viewId,
               rowId: widget.rowController.rowId,
@@ -68,104 +74,4 @@ class _RowDetailPageState extends State<RowDetailPage> {
       ),
     );
   }
-
-  Widget _rowBanner() {
-    return BlocBuilder<RowDetailBloc, RowDetailState>(
-      builder: (context, state) {
-        final paddingOffset = getHorizontalPadding(context);
-        return Padding(
-          padding: EdgeInsets.only(
-            left: paddingOffset,
-            right: paddingOffset,
-            top: 20,
-          ),
-          child: RowBanner(
-            rowMeta: widget.rowController.rowMeta,
-            viewId: widget.rowController.viewId,
-            cellBuilder: (fieldId) {
-              final fieldInfo = state.cells
-                  .firstWhereOrNull(
-                    (e) => e.fieldInfo.field.id == fieldId,
-                  )
-                  ?.fieldInfo;
-
-              if (fieldInfo != null) {
-                final style = GridTextCellStyle(
-                  placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
-                  textStyle: Theme.of(context).textTheme.titleLarge,
-                  showEmoji: false,
-                  autofocus: true,
-                );
-                final cellContext = DatabaseCellContext(
-                  viewId: widget.rowController.viewId,
-                  rowMeta: widget.rowController.rowMeta,
-                  fieldInfo: fieldInfo,
-                );
-                return widget.cellBuilder.build(cellContext, style: style);
-              } else {
-                return const SizedBox.shrink();
-              }
-            },
-          ),
-        );
-      },
-    );
-  }
-
-  Widget _responsiveRowInfo() {
-    final rowDataColumn = RowPropertyList(
-      cellBuilder: widget.cellBuilder,
-      viewId: widget.rowController.viewId,
-    );
-    final rowOptionColumn = RowActionList(
-      viewId: widget.rowController.viewId,
-      rowController: widget.rowController,
-    );
-    final paddingOffset = getHorizontalPadding(context);
-    if (MediaQuery.of(context).size.width > 800) {
-      return Row(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          Flexible(
-            flex: 3,
-            child: Padding(
-              padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
-              child: rowDataColumn,
-            ),
-          ),
-          const VerticalDivider(width: 1.0),
-          Flexible(
-            child: Padding(
-              padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
-              child: rowOptionColumn,
-            ),
-          ),
-        ],
-      );
-    } else {
-      return Column(
-        crossAxisAlignment: CrossAxisAlignment.stretch,
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          Padding(
-            padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
-            child: rowDataColumn,
-          ),
-          const Divider(height: 1.0),
-          Padding(
-            padding: EdgeInsets.symmetric(horizontal: paddingOffset),
-            child: rowOptionColumn,
-          )
-        ],
-      );
-    }
-  }
-}
-
-double getHorizontalPadding(BuildContext context) {
-  if (MediaQuery.of(context).size.width > 800) {
-    return 50;
-  } else {
-    return 20;
-  }
 }

+ 201 - 39
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart

@@ -1,21 +1,27 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
-import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
 import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'accessory/cell_accessory.dart';
 import 'cell_builder.dart';
+import 'cells/checkbox_cell/checkbox_cell.dart';
 import 'cells/date_cell/date_cell.dart';
+import 'cells/number_cell/number_cell.dart';
 import 'cells/select_option_cell/select_option_cell.dart';
 import 'cells/text_cell/text_cell.dart';
 import 'cells/timestamp_cell/timestamp_cell.dart';
@@ -23,7 +29,6 @@ import 'cells/url_cell/url_cell.dart';
 
 /// Display the row properties in a list. Only use this widget in the
 /// [RowDetailPage].
-///
 class RowPropertyList extends StatelessWidget {
   final String viewId;
   final GridCellBuilder cellBuilder;
@@ -38,39 +43,88 @@ class RowPropertyList extends StatelessWidget {
     return BlocBuilder<RowDetailBloc, RowDetailState>(
       buildWhen: (previous, current) => previous.cells != current.cells,
       builder: (context, state) {
-        return Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            // The rest of the fields are displayed in the order of the field
-            // list
-            ...state.cells
-                .where((element) => !element.fieldInfo.field.isPrimary)
-                .map(
-                  (cell) => _PropertyCell(
-                    cellContext: cell,
-                    cellBuilder: cellBuilder,
-                  ),
-                )
-                .toList(),
-            const VSpace(20),
-
-            // Create a new property(field) button
-            CreateRowFieldButton(viewId: viewId),
-          ],
+        final children = state.cells
+            .where((element) => !element.fieldInfo.field.isPrimary)
+            .mapIndexed(
+              (index, cell) => _PropertyCell(
+                key: ValueKey('row_detail_${cell.fieldId}'),
+                cellContext: cell,
+                cellBuilder: cellBuilder,
+                index: index,
+              ),
+            )
+            .toList();
+        return ReorderableListView(
+          shrinkWrap: true,
+          physics: const NeverScrollableScrollPhysics(),
+          onReorder: (oldIndex, newIndex) {
+            final reorderedField = children[oldIndex].cellContext.fieldId;
+            _reorderField(
+              context,
+              state.cells,
+              reorderedField,
+              oldIndex,
+              newIndex,
+            );
+          },
+          buildDefaultDragHandles: false,
+          proxyDecorator: (child, index, animation) => Material(
+            color: Colors.transparent,
+            child: Stack(
+              children: [
+                child,
+                const MouseRegion(cursor: SystemMouseCursors.grabbing),
+              ],
+            ),
+          ),
+          footer: Padding(
+            padding: const EdgeInsets.only(left: 20),
+            child: CreateRowFieldButton(viewId: viewId),
+          ),
+          children: children,
         );
       },
     );
   }
+
+  void _reorderField(
+    BuildContext context,
+    List<DatabaseCellContext> cells,
+    String reorderedFieldId,
+    int oldIndex,
+    int newIndex,
+  ) {
+    // when reorderiing downwards, need to update index
+    if (oldIndex < newIndex) {
+      newIndex--;
+    }
+
+    // also update index when the index is after the index of the primary field
+    // in the original list of DatabaseCellContext's
+    final primaryFieldIndex =
+        cells.indexWhere((element) => element.fieldInfo.isPrimary);
+    if (oldIndex >= primaryFieldIndex) {
+      oldIndex++;
+    }
+    if (newIndex >= primaryFieldIndex) {
+      newIndex++;
+    }
+
+    context.read<RowDetailBloc>().add(
+          RowDetailEvent.reorderField(reorderedFieldId, oldIndex, newIndex),
+        );
+  }
 }
 
 class _PropertyCell extends StatefulWidget {
   final DatabaseCellContext cellContext;
   final GridCellBuilder cellBuilder;
+  final int index;
   const _PropertyCell({
     required this.cellContext,
     required this.cellBuilder,
     Key? key,
+    required this.index,
   }) : super(key: key);
 
   @override
@@ -78,45 +132,65 @@ class _PropertyCell extends StatefulWidget {
 }
 
 class _PropertyCellState extends State<_PropertyCell> {
-  final PopoverController popover = PopoverController();
+  final PopoverController _popoverController = PopoverController();
+  bool _isFieldHover = false;
 
   @override
   Widget build(BuildContext context) {
     final style = _customCellStyle(widget.cellContext.fieldType);
     final cell = widget.cellBuilder.build(widget.cellContext, style: style);
 
+    final dragThumb = MouseRegion(
+      cursor: SystemMouseCursors.grab,
+      child: SizedBox(
+        width: 16,
+        height: 30,
+        child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
+      ),
+    );
+
     final gesture = GestureDetector(
       behavior: HitTestBehavior.opaque,
       onTap: () => cell.requestFocus.notify(),
-      child: AccessoryHover(
-        contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
-        child: cell,
-      ),
+      child: AccessoryHover(child: cell),
     );
 
-    return IntrinsicHeight(
-      child: ConstrainedBox(
-        constraints: const BoxConstraints(minHeight: 30),
+    return Container(
+      margin: const EdgeInsets.only(bottom: 8),
+      constraints: const BoxConstraints(minHeight: 30),
+      child: MouseRegion(
+        onEnter: (event) => setState(() => _isFieldHover = true),
+        onExit: (event) => setState(() => _isFieldHover = false),
         child: Row(
           crossAxisAlignment: CrossAxisAlignment.start,
           mainAxisAlignment: MainAxisAlignment.start,
           children: [
+            ReorderableDragStartListener(
+              index: widget.index,
+              enabled: _isFieldHover,
+              child: dragThumb,
+            ),
+            const HSpace(4),
             AppFlowyPopover(
-              controller: popover,
+              controller: _popoverController,
               constraints: BoxConstraints.loose(const Size(240, 600)),
               margin: EdgeInsets.zero,
               triggerActions: PopoverTriggerFlags.none,
+              direction: PopoverDirection.bottomWithLeftAligned,
               popupBuilder: (popoverContext) => buildFieldEditor(),
               child: SizedBox(
-                width: 150,
-                height: 40,
+                width: 160,
+                height: 30,
                 child: FieldCellButton(
                   field: widget.cellContext.fieldInfo.field,
-                  onTap: () => popover.show(),
+                  onTap: () => _popoverController.show(),
                   radius: BorderRadius.circular(6),
+                  margin:
+                      const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
                 ),
               ),
             ),
+            const HSpace(8),
             Expanded(child: gesture),
           ],
         ),
@@ -133,11 +207,11 @@ class _PropertyCellState extends State<_PropertyCell> {
         field: widget.cellContext.fieldInfo.field,
       ),
       onHidden: (fieldId) {
-        popover.close();
+        _popoverController.close();
         context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
       },
       onDeleted: (fieldId) {
-        popover.close();
+        _popoverController.close();
 
         NavigatorAlertDialog(
           title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
@@ -155,26 +229,35 @@ class _PropertyCellState extends State<_PropertyCell> {
 GridCellStyle? _customCellStyle(FieldType fieldType) {
   switch (fieldType) {
     case FieldType.Checkbox:
-      return null;
+      return GridCheckboxCellStyle(
+        cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
+      );
     case FieldType.DateTime:
       return DateCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
         alignment: Alignment.centerLeft,
+        cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
       );
     case FieldType.LastEditedTime:
     case FieldType.CreatedTime:
       return TimestampCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
         alignment: Alignment.centerLeft,
+        cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
       );
     case FieldType.MultiSelect:
       return SelectOptionCellStyle(
         placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+        cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
       );
     case FieldType.Checklist:
       return SelectOptionCellStyle(
         placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
       );
     case FieldType.Number:
-      return null;
+      return GridNumberCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
     case FieldType.RichText:
       return GridTextCellStyle(
         placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
@@ -182,6 +265,7 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
     case FieldType.SingleSelect:
       return SelectOptionCellStyle(
         placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+        cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
       );
 
     case FieldType.URL:
@@ -195,3 +279,81 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
   }
   throw UnimplementedError;
 }
+
+class CreateRowFieldButton extends StatefulWidget {
+  final String viewId;
+
+  const CreateRowFieldButton({required this.viewId, super.key});
+
+  @override
+  State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
+}
+
+class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
+  late PopoverController popoverController;
+  late TypeOptionPB typeOption;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      constraints: BoxConstraints.loose(const Size(240, 200)),
+      controller: popoverController,
+      direction: PopoverDirection.topWithLeftAligned,
+      triggerActions: PopoverTriggerFlags.none,
+      margin: EdgeInsets.zero,
+      child: SizedBox(
+        height: 30,
+        child: FlowyButton(
+          margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
+          text: FlowyText.medium(
+            LocaleKeys.grid_field_newProperty.tr(),
+            color: Theme.of(context).hintColor,
+          ),
+          hoverColor: AFThemeExtension.of(context).lightGreyHover,
+          onTap: () async {
+            final result = await TypeOptionBackendService.createFieldTypeOption(
+              viewId: widget.viewId,
+            );
+            result.fold(
+              (l) {
+                typeOption = l;
+                popoverController.show();
+              },
+              (r) => Log.error("Failed to create field type option: $r"),
+            );
+          },
+          leftIcon: FlowySvg(
+            FlowySvgs.add_m,
+            color: Theme.of(context).hintColor,
+          ),
+        ),
+      ),
+      popupBuilder: (BuildContext popOverContext) {
+        return FieldEditor(
+          viewId: widget.viewId,
+          typeOptionLoader: FieldTypeOptionLoader(
+            viewId: widget.viewId,
+            field: typeOption.field_2,
+          ),
+          onDeleted: (fieldId) {
+            popoverController.close();
+            NavigatorAlertDialog(
+              title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+              confirm: () {
+                context
+                    .read<RowDetailBloc>()
+                    .add(RowDetailEvent.deleteField(fieldId));
+              },
+            ).show(context);
+          },
+        );
+      },
+    );
+  }
+}

+ 5 - 0
frontend/resources/flowy_icons/16x/details_horizontal.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="4" cy="8" r="1" fill="#333333"/>
+<circle cx="8" cy="8" r="1" fill="#333333"/>
+<circle cx="12" cy="8" r="1" fill="#333333"/>
+</svg>

+ 13 - 0
frontend/resources/flowy_icons/16x/emoji.svg

@@ -0,0 +1,13 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_160_12007)">
+<path d="M8 12.998C10.7614 12.998 13 10.7595 13 7.99805C13 5.23662 10.7614 2.99805 8 2.99805C5.23857 2.99805 3 5.23662 3 7.99805C3 10.7595 5.23857 12.998 8 12.998Z" stroke="#333333" stroke-linejoin="round"/>
+<path d="M9.75 9.74805C9.75 9.74805 9.25 10.748 8 10.748C6.75 10.748 6.25 9.74805 6.25 9.74805" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.25 6.99805H9.25" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.25 6.49805V7.49805" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_160_12007">
+<rect width="12" height="12" fill="white" transform="translate(2 1.99805)"/>
+</clipPath>
+</defs>
+</svg>