Browse Source

chore: config row action sheet

appflowy 3 years ago
parent
commit
b93feb49c3
20 changed files with 380 additions and 92 deletions
  1. 4 0
      frontend/app_flowy/assets/translations/en.json
  2. 0 1
      frontend/app_flowy/lib/startup/deps_resolver.dart
  3. 14 14
      frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart
  4. 0 1
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  5. 1 1
      frontend/app_flowy/lib/workspace/application/grid/prelude.dart
  6. 58 0
      frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart
  7. 22 32
      frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart
  8. 17 5
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  9. 18 12
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  10. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart
  11. 5 15
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart
  12. 30 6
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  13. 133 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart
  14. 34 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart
  15. 4 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart
  16. 3 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart
  17. 16 0
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  18. 8 0
      frontend/rust-lib/flowy-grid/src/event_map.rs
  19. 10 3
      frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs
  20. 2 0
      frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto

+ 4 - 0
frontend/app_flowy/assets/translations/en.json

@@ -174,6 +174,10 @@
       "addOption": "Add option",
       "editProperty": "Edit property"
     },
+    "row": {
+      "duplicate": "Duplicate",
+      "delete": "Delete"
+    },
     "selectOption": {
       "purpleColor": "Purple",
       "pinkColor": "Pink",

+ 0 - 1
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -154,7 +154,6 @@ void _resolveGridDeps(GetIt getIt) {
   getIt.registerFactoryParam<RowBloc, RowData, void>(
     (data, _) => RowBloc(
       rowData: data,
-      rowlistener: RowListener(rowId: data.rowId),
     ),
   );
 

+ 14 - 14
frontend/app_flowy/lib/workspace/application/grid/field/action_sheet_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart

@@ -5,14 +5,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import 'field_service.dart';
 
-part 'action_sheet_bloc.freezed.dart';
+part 'field_action_sheet_bloc.freezed.dart';
 
-class FieldActionSheetBloc extends Bloc<ActionSheetEvent, ActionSheetState> {
+class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
   final FieldService service;
 
   FieldActionSheetBloc({required Field field, required this.service})
-      : super(ActionSheetState.initial(EditFieldContext.create()..gridField = field)) {
-    on<ActionSheetEvent>(
+      : super(FieldActionSheetState.initial(EditFieldContext.create()..gridField = field)) {
+    on<FieldActionSheetEvent>(
       (event, emit) async {
         await event.map(
           updateFieldName: (_UpdateFieldName value) async {
@@ -56,23 +56,23 @@ class FieldActionSheetBloc extends Bloc<ActionSheetEvent, ActionSheetState> {
 }
 
 @freezed
-class ActionSheetEvent with _$ActionSheetEvent {
-  const factory ActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
-  const factory ActionSheetEvent.hideField() = _HideField;
-  const factory ActionSheetEvent.duplicateField() = _DuplicateField;
-  const factory ActionSheetEvent.deleteField() = _DeleteField;
-  const factory ActionSheetEvent.saveField() = _SaveField;
+class FieldActionSheetEvent with _$FieldActionSheetEvent {
+  const factory FieldActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
+  const factory FieldActionSheetEvent.hideField() = _HideField;
+  const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
+  const factory FieldActionSheetEvent.deleteField() = _DeleteField;
+  const factory FieldActionSheetEvent.saveField() = _SaveField;
 }
 
 @freezed
-class ActionSheetState with _$ActionSheetState {
-  const factory ActionSheetState({
+class FieldActionSheetState with _$FieldActionSheetState {
+  const factory FieldActionSheetState({
     required EditFieldContext editContext,
     required String errorText,
     required String fieldName,
-  }) = _ActionSheetState;
+  }) = _FieldActionSheetState;
 
-  factory ActionSheetState.initial(EditFieldContext editContext) => ActionSheetState(
+  factory FieldActionSheetState.initial(EditFieldContext editContext) => FieldActionSheetState(
         editContext: editContext,
         errorText: '',
         fieldName: editContext.gridField.name,

+ 0 - 1
frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart

@@ -111,7 +111,6 @@ class GridBloc extends Bloc<GridEvent, GridState> {
       rows.addAll(gridBlock.rowOrders.map(
         (rowOrder) => GridBlockRow(
           gridId: view.id,
-          blockId: gridBlock.id,
           rowId: rowOrder.rowId,
           height: rowOrder.height.toDouble(),
         ),

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/prelude.dart

@@ -7,7 +7,7 @@ export 'data.dart';
 // Field
 export 'field/field_service.dart';
 export 'field/grid_header_bloc.dart';
-export 'field/action_sheet_bloc.dart';
+export 'field/field_action_sheet_bloc.dart';
 export 'field/field_editor_bloc.dart';
 export 'field/field_switch_bloc.dart';
 

+ 58 - 0
frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart

@@ -0,0 +1,58 @@
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+
+part 'row_action_sheet_bloc.freezed.dart';
+
+class RowActionSheetBloc extends Bloc<RowActionSheetEvent, RowActionSheetState> {
+  final RowService _rowService;
+
+  RowActionSheetBloc({required RowData rowData})
+      : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
+        super(RowActionSheetState.initial(rowData)) {
+    on<RowActionSheetEvent>(
+      (event, emit) async {
+        await event.map(
+          deleteRow: (_DeleteRow value) async {
+            final result = await _rowService.deleteRow();
+            logResult(result);
+          },
+          duplicateRow: (_DuplicateRow value) async {
+            final result = await _rowService.duplicateRow();
+            logResult(result);
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+
+  void logResult(Either<Unit, FlowyError> result) {
+    result.fold((l) => null, (err) => Log.error(err));
+  }
+}
+
+@freezed
+class RowActionSheetEvent with _$RowActionSheetEvent {
+  const factory RowActionSheetEvent.duplicateRow() = _DuplicateRow;
+  const factory RowActionSheetEvent.deleteRow() = _DeleteRow;
+}
+
+@freezed
+class RowActionSheetState with _$RowActionSheetState {
+  const factory RowActionSheetState({
+    required RowData rowData,
+  }) = _RowActionSheetState;
+
+  factory RowActionSheetState.initial(RowData rowData) => RowActionSheetState(
+        rowData: rowData,
+      );
+}

+ 22 - 32
frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

@@ -1,7 +1,6 @@
 import 'dart:collection';
 
 import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
-import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,19 +15,14 @@ part 'row_bloc.freezed.dart';
 typedef CellDataMap = LinkedHashMap<String, CellData>;
 
 class RowBloc extends Bloc<RowEvent, RowState> {
-  final RowService rowService;
-  final RowListener rowlistener;
-  final GridFieldsListener fieldListener;
-
-  RowBloc({required RowData rowData, required this.rowlistener})
-      : rowService = RowService(
-          gridId: rowData.gridId,
-          blockId: rowData.blockId,
-          rowId: rowData.rowId,
-        ),
-        fieldListener = GridFieldsListener(
-          gridId: rowData.gridId,
-        ),
+  final RowService _rowService;
+  final RowListener _rowlistener;
+  final GridFieldsListener _fieldListener;
+
+  RowBloc({required RowData rowData})
+      : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
+        _fieldListener = GridFieldsListener(gridId: rowData.gridId),
+        _rowlistener = RowListener(rowId: rowData.rowId),
         super(RowState.initial(rowData)) {
     on<RowEvent>(
       (event, emit) async {
@@ -38,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             await _loadRow(emit);
           },
           createRow: (_CreateRow value) {
-            rowService.createRow();
+            _rowService.createRow();
           },
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) async {
             await _handleFieldUpdate(emit, value);
@@ -52,7 +46,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
   }
 
   void _handleRowUpdate(_DidUpdateRow value, Emitter<RowState> emit) {
-    final CellDataMap cellDataMap = _makeCellDatas(value.row, state.fields);
+    final CellDataMap cellDataMap = _makeCellDatas(value.row, state.rowData.fields);
     emit(state.copyWith(
       row: Future(() => Some(value.row)),
       cellDataMap: Some(cellDataMap),
@@ -67,39 +61,39 @@ class RowBloc extends Bloc<RowEvent, RowState> {
     );
 
     emit(state.copyWith(
-      fields: value.fields,
+      rowData: state.rowData.copyWith(fields: value.fields),
       cellDataMap: Some(cellDataMap),
     ));
   }
 
   @override
   Future<void> close() async {
-    await rowlistener.stop();
-    await fieldListener.stop();
+    await _rowlistener.stop();
+    await _fieldListener.stop();
     return super.close();
   }
 
   Future<void> _startListening() async {
-    rowlistener.updateRowNotifier.addPublishListener((result) {
+    _rowlistener.updateRowNotifier.addPublishListener((result) {
       result.fold(
         (row) => add(RowEvent.didUpdateRow(row)),
         (err) => Log.error(err),
       );
     });
 
-    fieldListener.updateFieldsNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
       result.fold(
         (fields) => add(RowEvent.didReceiveFieldUpdate(fields)),
         (err) => Log.error(err),
       );
     });
 
-    rowlistener.start();
-    fieldListener.start();
+    _rowlistener.start();
+    _fieldListener.start();
   }
 
   Future<void> _loadRow(Emitter<RowState> emit) async {
-    rowService.getRow().then((result) {
+    _rowService.getRow().then((result) {
       return result.fold(
         (row) => add(RowEvent.didUpdateRow(row)),
         (err) => Log.error(err),
@@ -113,7 +107,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
       if (field.visibility) {
         map[field.id] = CellData(
           rowId: row.id,
-          gridId: rowService.gridId,
+          gridId: _rowService.gridId,
           cell: row.cellByFieldId[field.id],
           field: field,
         );
@@ -134,17 +128,13 @@ class RowEvent with _$RowEvent {
 @freezed
 class RowState with _$RowState {
   const factory RowState({
-    required String rowId,
-    required double rowHeight,
-    required List<Field> fields,
+    required RowData rowData,
     required Future<Option<Row>> row,
     required Option<CellDataMap> cellDataMap,
   }) = _RowState;
 
-  factory RowState.initial(RowData data) => RowState(
-        rowId: data.rowId,
-        rowHeight: data.height,
-        fields: data.fields,
+  factory RowState.initial(RowData rowData) => RowState(
+        rowData: rowData,
         row: Future(() => none()),
         cellDataMap: none(),
       );

+ 17 - 5
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -10,9 +10,8 @@ part 'row_service.freezed.dart';
 class RowService {
   final String gridId;
   final String rowId;
-  final String blockId;
 
-  RowService({required this.gridId, required this.rowId, required this.blockId});
+  RowService({required this.gridId, required this.rowId});
 
   Future<Either<Row, FlowyError>> createRow() {
     CreateRowPayload payload = CreateRowPayload.create()
@@ -29,6 +28,22 @@ class RowService {
 
     return GridEventGetRow(payload).send();
   }
+
+  Future<Either<Unit, FlowyError>> deleteRow() {
+    final payload = RowIdentifierPayload.create()
+      ..gridId = gridId
+      ..rowId = rowId;
+
+    return GridEventDeleteRow(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> duplicateRow() {
+    final payload = RowIdentifierPayload.create()
+      ..gridId = gridId
+      ..rowId = rowId;
+
+    return GridEventDuplicateRow(payload).send();
+  }
 }
 
 @freezed
@@ -46,7 +61,6 @@ class RowData with _$RowData {
   const factory RowData({
     required String gridId,
     required String rowId,
-    required String blockId,
     required List<Field> fields,
     required double height,
   }) = _RowData;
@@ -55,7 +69,6 @@ class RowData with _$RowData {
     return RowData(
       gridId: row.gridId,
       rowId: row.rowId,
-      blockId: row.blockId,
       fields: fields,
       height: row.height,
     );
@@ -67,7 +80,6 @@ class GridBlockRow with _$GridBlockRow {
   const factory GridBlockRow({
     required String gridId,
     required String rowId,
-    required String blockId,
     required double height,
   }) = _GridBlockRow;
 }

+ 18 - 12
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -73,6 +73,7 @@ class FlowyGrid extends StatefulWidget {
 
 class _FlowyGridState extends State<FlowyGrid> {
   final _scrollController = GridScrollController();
+  final _key = GlobalKey<SliverAnimatedListState>();
 
   @override
   void dispose() {
@@ -91,6 +92,7 @@ class _FlowyGridState extends State<FlowyGrid> {
           return const Center(child: CircularProgressIndicator.adaptive());
         }
 
+// _key.currentState.insertItem(index)
         final child = BlocBuilder<GridBloc, GridState>(
           builder: (context, state) {
             return SizedBox(
@@ -153,20 +155,24 @@ class _FlowyGridState extends State<FlowyGrid> {
         return rowChanged;
       },
       builder: (context, state) {
-        return SliverList(
-          delegate: SliverChildBuilderDelegate(
-            (context, index) {
-              final blockRow = context.read<GridBloc>().state.rows[index];
-              final fields = context.read<GridBloc>().state.fields;
-              final rowData = RowData.fromBlockRow(blockRow, fields);
-              return GridRowWidget(data: rowData, key: ValueKey(rowData.rowId));
-            },
-            childCount: context.read<GridBloc>().state.rows.length,
-            addRepaintBoundaries: true,
-            addAutomaticKeepAlives: true,
-          ),
+        return SliverAnimatedList(
+          key: _key,
+          initialItemCount: context.read<GridBloc>().state.rows.length,
+          itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+            final blockRow = context.read<GridBloc>().state.rows[index];
+            final fields = context.read<GridBloc>().state.fields;
+            final rowData = RowData.fromBlockRow(blockRow, fields);
+            return _renderRow(rowData, animation);
+          },
         );
       },
     );
   }
+
+  Widget _renderRow(RowData rowData, Animation<double> animation) {
+    return SizeTransition(
+      sizeFactor: animation,
+      child: GridRowWidget(data: rowData, key: ValueKey(rowData.rowId)),
+    );
+  }
 }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart

@@ -6,7 +6,7 @@ class GridSize {
   static double get scrollBarSize => 12 * scale;
   static double get headerHeight => 40 * scale;
   static double get footerHeight => 40 * scale;
-  static double get leadingHeaderPadding => 30 * scale;
+  static double get leadingHeaderPadding => 50 * scale;
   static double get trailHeaderPadding => 140 * scale;
   static double get headerContainerPadding => 0 * scale;
   static double get cellHPadding => 10 * scale;

+ 5 - 15
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart

@@ -23,7 +23,7 @@ class GridFieldCellActionSheet extends StatelessWidget with FlowyOverlayDelegate
         child: this,
         constraints: BoxConstraints.loose(const Size(240, 200)),
       ),
-      identifier: identifier(),
+      identifier: GridFieldCellActionSheet.identifier(),
       anchorContext: overlayContext,
       anchorDirection: AnchorDirection.bottomWithLeftAligned,
       delegate: this,
@@ -68,7 +68,7 @@ class _EditFieldButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return BlocBuilder<FieldActionSheetBloc, ActionSheetState>(
+    return BlocBuilder<FieldActionSheetBloc, FieldActionSheetState>(
       builder: (context, state) {
         return SizedBox(
           height: GridSize.typeOptionItemHeight,
@@ -100,16 +100,6 @@ class _FieldOperationList extends StatelessWidget {
         )
         .toList();
 
-    return FieldOperationList(actions: actions);
-  }
-}
-
-class FieldOperationList extends StatelessWidget {
-  final List<FieldActionCell> actions;
-  const FieldOperationList({required this.actions, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
     return GridView(
       // https://api.flutter.dev/flutter/widgets/AnimatedList/shrinkWrap.html
       shrinkWrap: true,
@@ -182,13 +172,13 @@ extension _FieldActionExtension on FieldAction {
   void run(BuildContext context) {
     switch (this) {
       case FieldAction.hide:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.hideField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.hideField());
         break;
       case FieldAction.duplicate:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.duplicateField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.duplicateField());
         break;
       case FieldAction.delete:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.deleteField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.deleteField());
         break;
     }
   }

+ 30 - 6
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

@@ -5,10 +5,13 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/p
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
 
+import 'row_action_sheet.dart';
+
 class GridRowWidget extends StatefulWidget {
   final RowData data;
   const GridRowWidget({required this.data, Key? key}) : super(key: key);
@@ -39,10 +42,10 @@ class _GridRowWidgetState extends State<GridRowWidget> {
           onEnter: (p) => _rowStateNotifier.onEnter = true,
           onExit: (p) => _rowStateNotifier.onEnter = false,
           child: BlocBuilder<RowBloc, RowState>(
-            buildWhen: (p, c) => p.rowHeight != c.rowHeight,
+            buildWhen: (p, c) => p.rowData.height != c.rowData.height,
             builder: (context, state) {
               return SizedBox(
-                height: _rowBloc.state.rowHeight,
+                height: _rowBloc.state.rowData.height,
                 child: Row(
                   crossAxisAlignment: CrossAxisAlignment.stretch,
                   children: const [
@@ -83,7 +86,8 @@ class _RowLeading extends StatelessWidget {
     return Row(
       mainAxisAlignment: MainAxisAlignment.center,
       children: const [
-        AppendRowButton(),
+        _InsertRowButton(),
+        _DeleteRowButton(),
       ],
     );
   }
@@ -98,15 +102,16 @@ class _RowTrailing extends StatelessWidget {
   }
 }
 
-class AppendRowButton extends StatelessWidget {
-  const AppendRowButton({Key? key}) : super(key: key);
+class _InsertRowButton extends StatelessWidget {
+  const _InsertRowButton({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyIconButton(
       hoverColor: theme.hover,
-      width: 22,
+      width: 20,
+      height: 30,
       onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
       iconPadding: const EdgeInsets.all(3),
       icon: svgWidget("home/add"),
@@ -114,6 +119,25 @@ class AppendRowButton extends StatelessWidget {
   }
 }
 
+class _DeleteRowButton extends StatelessWidget {
+  const _DeleteRowButton({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return FlowyIconButton(
+      hoverColor: theme.hover,
+      width: 20,
+      height: 30,
+      onPressed: () => GridRowActionSheet(
+        rowData: context.read<RowBloc>().state.rowData,
+      ).show(context),
+      iconPadding: const EdgeInsets.all(3),
+      icon: svgWidget("editor/details"),
+    );
+  }
+}
+
 class _RowCells extends StatelessWidget {
   const _RowCells({Key? key}) : super(key: key);
 

+ 133 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart

@@ -0,0 +1,133 @@
+import 'package:app_flowy/workspace/application/grid/row/row_action_sheet_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class GridRowActionSheet extends StatelessWidget {
+  final RowData rowData;
+  const GridRowActionSheet({required this.rowData, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => RowActionSheetBloc(rowData: rowData),
+      child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
+        builder: (context, state) {
+          final cells = _RowAction.values
+              .map(
+                (action) => _RowActionCell(
+                  action: action,
+                  onDismissed: () => remove(context),
+                ),
+              )
+              .toList();
+
+          //
+          final list = ListView.separated(
+            shrinkWrap: true,
+            controller: ScrollController(),
+            itemCount: cells.length,
+            separatorBuilder: (context, index) {
+              return VSpace(GridSize.typeOptionSeparatorHeight);
+            },
+            physics: StyledScrollPhysics(),
+            itemBuilder: (BuildContext context, int index) {
+              return cells[index];
+            },
+          );
+          return list;
+        },
+      ),
+    );
+  }
+
+  void show(BuildContext overlayContext) {
+    FlowyOverlay.of(overlayContext).insertWithAnchor(
+      widget: OverlayContainer(
+        child: this,
+        constraints: BoxConstraints.loose(const Size(140, 200)),
+      ),
+      identifier: GridRowActionSheet.identifier(),
+      anchorContext: overlayContext,
+      anchorDirection: AnchorDirection.leftWithCenterAligned,
+    );
+  }
+
+  void remove(BuildContext overlayContext) {
+    FlowyOverlay.of(overlayContext).remove(GridRowActionSheet.identifier());
+  }
+
+  static String identifier() {
+    return (GridRowActionSheet).toString();
+  }
+}
+
+class _RowActionCell extends StatelessWidget {
+  final _RowAction action;
+  final VoidCallback onDismissed;
+  const _RowActionCell({required this.action, required this.onDismissed, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(action.title(), fontSize: 12),
+        hoverColor: theme.hover,
+        onTap: () {
+          action.performAction(context);
+          onDismissed();
+        },
+        leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
+      ),
+    );
+  }
+}
+
+enum _RowAction {
+  delete,
+  duplicate,
+}
+
+extension _RowActionExtension on _RowAction {
+  String iconName() {
+    switch (this) {
+      case _RowAction.duplicate:
+        return 'grid/duplicate';
+      case _RowAction.delete:
+        return 'grid/delete';
+    }
+  }
+
+  String title() {
+    switch (this) {
+      case _RowAction.duplicate:
+        return LocaleKeys.grid_row_duplicate.tr();
+      case _RowAction.delete:
+        return LocaleKeys.grid_row_delete.tr();
+    }
+  }
+
+  void performAction(BuildContext context) {
+    switch (this) {
+      case _RowAction.duplicate:
+        // context.read<RowActionSheetBloc>().add(const RowActionSheetEvent.duplicateRow());
+        break;
+      case _RowAction.delete:
+        // context.read<RowActionSheetBloc>().add(const RowActionSheetEvent.deleteRow());
+        break;
+    }
+  }
+}

+ 34 - 0
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart

@@ -239,6 +239,40 @@ class GridEventGetRow {
     }
 }
 
+class GridEventDeleteRow {
+     RowIdentifierPayload request;
+     GridEventDeleteRow(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.DeleteRow.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
+class GridEventDuplicateRow {
+     RowIdentifierPayload request;
+     GridEventDuplicateRow(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.DuplicateRow.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class GridEventGetCell {
      CellIdentifierPayload request;
      GridEventGetCell(this.request);

+ 4 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart

@@ -24,6 +24,8 @@ class GridEvent extends $pb.ProtobufEnum {
   static const GridEvent ApplySelectOptionChangeset = GridEvent._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplySelectOptionChangeset');
   static const GridEvent CreateRow = GridEvent._(50, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateRow');
   static const GridEvent GetRow = GridEvent._(51, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetRow');
+  static const GridEvent DeleteRow = GridEvent._(52, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteRow');
+  static const GridEvent DuplicateRow = GridEvent._(53, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DuplicateRow');
   static const GridEvent GetCell = GridEvent._(70, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetCell');
   static const GridEvent UpdateCell = GridEvent._(71, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateCell');
   static const GridEvent ApplySelectOptionCellChangeset = GridEvent._(72, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplySelectOptionCellChangeset');
@@ -43,6 +45,8 @@ class GridEvent extends $pb.ProtobufEnum {
     ApplySelectOptionChangeset,
     CreateRow,
     GetRow,
+    DeleteRow,
+    DuplicateRow,
     GetCell,
     UpdateCell,
     ApplySelectOptionCellChangeset,

+ 3 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart

@@ -26,6 +26,8 @@ const GridEvent$json = const {
     const {'1': 'ApplySelectOptionChangeset', '2': 32},
     const {'1': 'CreateRow', '2': 50},
     const {'1': 'GetRow', '2': 51},
+    const {'1': 'DeleteRow', '2': 52},
+    const {'1': 'DuplicateRow', '2': 53},
     const {'1': 'GetCell', '2': 70},
     const {'1': 'UpdateCell', '2': 71},
     const {'1': 'ApplySelectOptionCellChangeset', '2': 72},
@@ -33,4 +35,4 @@ const GridEvent$json = const {
 };
 
 /// Descriptor for `GridEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEhMKD05ld1NlbGVjdE9wdGlvbhAeEhoKFkdldFNlbGVjdE9wdGlvbkNvbnRleHQQHxIeChpBcHBseVNlbGVjdE9wdGlvbkNoYW5nZXNldBAgEg0KCUNyZWF0ZVJvdxAyEgoKBkdldFJvdxAzEgsKB0dldENlbGwQRhIOCgpVcGRhdGVDZWxsEEcSIgoeQXBwbHlTZWxlY3RPcHRpb25DZWxsQ2hhbmdlc2V0EEg=');
+final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEhMKD05ld1NlbGVjdE9wdGlvbhAeEhoKFkdldFNlbGVjdE9wdGlvbkNvbnRleHQQHxIeChpBcHBseVNlbGVjdE9wdGlvbkNoYW5nZXNldBAgEg0KCUNyZWF0ZVJvdxAyEgoKBkdldFJvdxAzEg0KCURlbGV0ZVJvdxA0EhAKDER1cGxpY2F0ZVJvdxA1EgsKB0dldENlbGwQRhIOCgpVcGRhdGVDZWxsEEcSIgoeQXBwbHlTZWxlY3RPcHRpb25DZWxsQ2hhbmdlc2V0EEg=');

+ 16 - 0
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -179,6 +179,22 @@ pub(crate) async fn get_row_handler(
     }
 }
 
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn delete_row_handler(
+    data: Data<RowIdentifierPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> DataResult<Row, FlowyError> {
+    todo!()
+}
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn duplicate_row_handler(
+    data: Data<RowIdentifierPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> DataResult<Row, FlowyError> {
+    todo!()
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn create_row_handler(
     data: Data<CreateRowPayload>,

+ 8 - 0
frontend/rust-lib/flowy-grid/src/event_map.rs

@@ -20,6 +20,8 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         // Row
         .event(GridEvent::CreateRow, create_row_handler)
         .event(GridEvent::GetRow, get_row_handler)
+        .event(GridEvent::DeleteRow, delete_row_handler)
+        .event(GridEvent::DuplicateRow, duplicate_row_handler)
         // Cell
         .event(GridEvent::GetCell, get_cell_handler)
         .event(GridEvent::UpdateCell, update_cell_handler)
@@ -81,6 +83,12 @@ pub enum GridEvent {
     #[event(input = "RowIdentifierPayload", output = "Row")]
     GetRow = 51,
 
+    #[event(input = "RowIdentifierPayload")]
+    DeleteRow = 52,
+
+    #[event(input = "RowIdentifierPayload")]
+    DuplicateRow = 53,
+
     #[event(input = "CellIdentifierPayload", output = "Cell")]
     GetCell = 70,
 

+ 10 - 3
frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs

@@ -39,6 +39,8 @@ pub enum GridEvent {
     ApplySelectOptionChangeset = 32,
     CreateRow = 50,
     GetRow = 51,
+    DeleteRow = 52,
+    DuplicateRow = 53,
     GetCell = 70,
     UpdateCell = 71,
     ApplySelectOptionCellChangeset = 72,
@@ -65,6 +67,8 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             32 => ::std::option::Option::Some(GridEvent::ApplySelectOptionChangeset),
             50 => ::std::option::Option::Some(GridEvent::CreateRow),
             51 => ::std::option::Option::Some(GridEvent::GetRow),
+            52 => ::std::option::Option::Some(GridEvent::DeleteRow),
+            53 => ::std::option::Option::Some(GridEvent::DuplicateRow),
             70 => ::std::option::Option::Some(GridEvent::GetCell),
             71 => ::std::option::Option::Some(GridEvent::UpdateCell),
             72 => ::std::option::Option::Some(GridEvent::ApplySelectOptionCellChangeset),
@@ -88,6 +92,8 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             GridEvent::ApplySelectOptionChangeset,
             GridEvent::CreateRow,
             GridEvent::GetRow,
+            GridEvent::DeleteRow,
+            GridEvent::DuplicateRow,
             GridEvent::GetCell,
             GridEvent::UpdateCell,
             GridEvent::ApplySelectOptionCellChangeset,
@@ -119,15 +125,16 @@ impl ::protobuf::reflect::ProtobufValue for GridEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fevent_map.proto*\xde\x02\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
+    \n\x0fevent_map.proto*\xff\x02\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
     \0\x12\x11\n\rGetGridBlocks\x10\x01\x12\r\n\tGetFields\x10\n\x12\x0f\n\
     \x0bUpdateField\x10\x0b\x12\x0f\n\x0bCreateField\x10\x0c\x12\x0f\n\x0bDe\
     leteField\x10\r\x12\x11\n\rSwitchToField\x10\x0e\x12\x12\n\x0eDuplicateF\
     ield\x10\x0f\x12\x17\n\x13GetEditFieldContext\x10\x10\x12\x13\n\x0fNewSe\
     lectOption\x10\x1e\x12\x1a\n\x16GetSelectOptionContext\x10\x1f\x12\x1e\n\
     \x1aApplySelectOptionChangeset\x10\x20\x12\r\n\tCreateRow\x102\x12\n\n\
-    \x06GetRow\x103\x12\x0b\n\x07GetCell\x10F\x12\x0e\n\nUpdateCell\x10G\x12\
-    \"\n\x1eApplySelectOptionCellChangeset\x10Hb\x06proto3\
+    \x06GetRow\x103\x12\r\n\tDeleteRow\x104\x12\x10\n\x0cDuplicateRow\x105\
+    \x12\x0b\n\x07GetCell\x10F\x12\x0e\n\nUpdateCell\x10G\x12\"\n\x1eApplySe\
+    lectOptionCellChangeset\x10Hb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 2 - 0
frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto

@@ -15,6 +15,8 @@ enum GridEvent {
     ApplySelectOptionChangeset = 32;
     CreateRow = 50;
     GetRow = 51;
+    DeleteRow = 52;
+    DuplicateRow = 53;
     GetCell = 70;
     UpdateCell = 71;
     ApplySelectOptionCellChangeset = 72;