浏览代码

chore: add option & color selection

appflowy 3 年之前
父节点
当前提交
065a72a8da
共有 40 个文件被更改,包括 828 次插入190 次删除
  1. 4 0
      frontend/app_flowy/assets/images/grid/details.svg
  2. 11 0
      frontend/app_flowy/assets/translations/en.json
  3. 8 2
      frontend/app_flowy/lib/startup/deps_resolver.dart
  4. 0 2
      frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart
  5. 42 0
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_bloc.dart
  6. 7 10
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart
  7. 0 39
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/selection_bloc.dart
  8. 57 0
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_bloc.dart
  9. 17 0
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart
  10. 1 1
      frontend/app_flowy/lib/workspace/application/grid/prelude.dart
  11. 1 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart
  12. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart
  13. 21 10
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart
  14. 115 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart
  15. 48 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart
  16. 57 80
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/option_pannel.dart
  17. 55 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart
  18. 35 12
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart
  19. 3 2
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  20. 6 0
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart
  21. 17 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart
  22. 1 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart
  23. 2 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbenum.dart
  24. 2 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbjson.dart
  25. 47 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  26. 10 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  27. 5 3
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart
  28. 5 4
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart
  29. 9 1
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  30. 7 3
      frontend/rust-lib/flowy-grid/src/event_map.rs
  31. 13 9
      frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs
  32. 4 3
      frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto
  33. 2 0
      shared-lib/flowy-error-code/src/code.rs
  34. 7 3
      shared-lib/flowy-error-code/src/protobuf/model/code.rs
  35. 1 0
      shared-lib/flowy-error-code/src/protobuf/proto/code.proto
  36. 22 1
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  37. 2 2
      shared-lib/flowy-grid-data-model/src/parser/mod.rs
  38. 18 0
      shared-lib/flowy-grid-data-model/src/parser/str_parser.rs
  39. 162 1
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  40. 3 0
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

+ 4 - 0
frontend/app_flowy/assets/images/grid/details.svg

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

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

@@ -167,6 +167,17 @@
       "addSelectOption": "Add an option",
       "optionTitle": "Options",
       "addOption": "Add option"
+    },
+    "selectOption": {
+      "purpleColor": "Purple",
+      "pinkColor": "Pink",
+      "lightPinkColor": "Light Pink",
+      "orangeColor": "Orange",
+      "yellowColor": "Yellow",
+      "limeColor": "Lime",
+      "greenColor": "Green",
+      "aquaColor": "Aqua",
+      "blueColor": "Blue"
     }
   }
 }

+ 8 - 2
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -3,6 +3,7 @@ import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/user/application/user_service.dart';
 import 'package:app_flowy/workspace/application/app/prelude.dart';
 import 'package:app_flowy/workspace/application/doc/prelude.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_listener.dart';
 import 'package:app_flowy/workspace/application/trash/prelude.dart';
@@ -20,6 +21,7 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
 import 'package:get_it/get_it.dart';
 
@@ -215,8 +217,12 @@ void _resolveGridDeps(GetIt getIt) {
     (context, _) => FieldTypeSwitchBloc(context),
   );
 
-  getIt.registerFactory<SelectionTypeOptionBloc>(
-    () => SelectionTypeOptionBloc(),
+  getIt.registerFactoryParam<SingleSelectTypeOptionBloc, SingleSelectTypeOption, String>(
+    (typeOption, fieldId) => SingleSelectTypeOptionBloc(typeOption, fieldId),
+  );
+
+  getIt.registerFactoryParam<MultiSelectTypeOptionBloc, MultiSelectTypeOption, void>(
+    (typeOption, _) => MultiSelectTypeOptionBloc(typeOption),
   );
 
   getIt.registerFactoryParam<DateTypeOptionBloc, DateTypeOption, void>(

+ 0 - 2
frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart

@@ -1,5 +1,3 @@
-import 'dart:typed_data';
-
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';

+ 42 - 0
frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_bloc.dart

@@ -0,0 +1,42 @@
+import 'dart:typed_data';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'multi_select_bloc.freezed.dart';
+
+class MultiSelectTypeOptionBloc extends Bloc<MultiSelectTypeOptionEvent, MultiSelectTypeOptionState> {
+  MultiSelectTypeOptionBloc(MultiSelectTypeOption typeOption) : super(MultiSelectTypeOptionState.initial(typeOption)) {
+    on<MultiSelectTypeOptionEvent>(
+      (event, emit) async {
+        await event.map(
+          createOption: (_CreateOption value) {},
+          updateOptions: (_UpdateOptions value) async {},
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+}
+
+@freezed
+class MultiSelectTypeOptionEvent with _$MultiSelectTypeOptionEvent {
+  const factory MultiSelectTypeOptionEvent.createOption(String optionName) = _CreateOption;
+  const factory MultiSelectTypeOptionEvent.updateOptions(List<SelectOption> options) = _UpdateOptions;
+}
+
+@freezed
+class MultiSelectTypeOptionState with _$MultiSelectTypeOptionState {
+  const factory MultiSelectTypeOptionState({
+    required MultiSelectTypeOption typeOption,
+  }) = _MultiSelectTypeOptionState;
+
+  factory MultiSelectTypeOptionState.initial(MultiSelectTypeOption typeOption) => MultiSelectTypeOptionState(
+        typeOption: typeOption,
+      );
+}

+ 7 - 10
frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart

@@ -1,13 +1,8 @@
-import 'dart:typed_data';
-
-import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.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 'option_pannel_bloc.freezed.dart';
 
 class OptionPannelBloc extends Bloc<OptionPannelEvent, OptionPannelState> {
@@ -16,13 +11,13 @@ class OptionPannelBloc extends Bloc<OptionPannelEvent, OptionPannelState> {
       (event, emit) async {
         await event.map(
           createOption: (_CreateOption value) async {
-            emit(state.copyWith(isAddingOption: false));
+            emit(state.copyWith(isEditingOption: false, newOptionName: Some(value.optionName)));
           },
           beginAddingOption: (_BeginAddingOption value) {
-            emit(state.copyWith(isAddingOption: true));
+            emit(state.copyWith(isEditingOption: true, newOptionName: none()));
           },
           endAddingOption: (_EndAddingOption value) {
-            emit(state.copyWith(isAddingOption: false));
+            emit(state.copyWith(isEditingOption: false, newOptionName: none()));
           },
         );
       },
@@ -46,11 +41,13 @@ class OptionPannelEvent with _$OptionPannelEvent {
 class OptionPannelState with _$OptionPannelState {
   const factory OptionPannelState({
     required List<SelectOption> options,
-    required bool isAddingOption,
+    required bool isEditingOption,
+    required Option<String> newOptionName,
   }) = _OptionPannelState;
 
   factory OptionPannelState.initial(List<SelectOption> options) => OptionPannelState(
         options: options,
-        isAddingOption: false,
+        isEditingOption: false,
+        newOptionName: none(),
       );
 }

+ 0 - 39
frontend/app_flowy/lib/workspace/application/grid/field/type_option/selection_bloc.dart

@@ -1,39 +0,0 @@
-import 'dart:typed_data';
-
-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';
-import 'package:freezed_annotation/freezed_annotation.dart';
-import 'dart:async';
-import 'package:dartz/dartz.dart';
-
-part 'selection_bloc.freezed.dart';
-
-class SelectionTypeOptionBloc extends Bloc<SelectionTypeOptionEvent, SelectionTypeOptionState> {
-  SelectionTypeOptionBloc() : super(SelectionTypeOptionState.initial()) {
-    on<SelectionTypeOptionEvent>(
-      (event, emit) async {
-        await event.map(
-          initial: (_InitialField value) async {},
-        );
-      },
-    );
-  }
-
-  @override
-  Future<void> close() async {
-    return super.close();
-  }
-}
-
-@freezed
-class SelectionTypeOptionEvent with _$SelectionTypeOptionEvent {
-  const factory SelectionTypeOptionEvent.initial(Uint8List? typeOptionData) = _InitialField;
-}
-
-@freezed
-class SelectionTypeOptionState with _$SelectionTypeOptionState {
-  const factory SelectionTypeOptionState() = _SelectionTypeOptionState;
-
-  factory SelectionTypeOptionState.initial() => SelectionTypeOptionState();
-}

+ 57 - 0
frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_bloc.dart

@@ -0,0 +1,57 @@
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import 'type_option_service.dart';
+
+part 'single_select_bloc.freezed.dart';
+
+class SingleSelectTypeOptionBloc extends Bloc<SingleSelectTypeOptionEvent, SingleSelectTypeOptionState> {
+  final TypeOptionService service;
+
+  SingleSelectTypeOptionBloc(SingleSelectTypeOption typeOption, String fieldId)
+      : service = TypeOptionService(fieldId: fieldId),
+        super(SingleSelectTypeOptionState.initial(typeOption)) {
+    on<SingleSelectTypeOptionEvent>(
+      (event, emit) async {
+        await event.map(
+          createOption: (_CreateOption value) async {
+            final result = await service.createOption(value.optionName);
+            result.fold(
+              (option) {
+                state.typeOption.options.insert(0, option);
+                emit(state);
+              },
+              (err) => Log.error(err),
+            );
+          },
+          updateOptions: (_UpdateOptions value) async {},
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+}
+
+@freezed
+class SingleSelectTypeOptionEvent with _$SingleSelectTypeOptionEvent {
+  const factory SingleSelectTypeOptionEvent.createOption(String optionName) = _CreateOption;
+  const factory SingleSelectTypeOptionEvent.updateOptions(List<SelectOption> options) = _UpdateOptions;
+}
+
+@freezed
+class SingleSelectTypeOptionState with _$SingleSelectTypeOptionState {
+  const factory SingleSelectTypeOptionState({
+    required SingleSelectTypeOption typeOption,
+  }) = _SingleSelectTypeOptionState;
+
+  factory SingleSelectTypeOptionState.initial(SingleSelectTypeOption typeOption) => SingleSelectTypeOptionState(
+        typeOption: typeOption,
+      );
+}

+ 17 - 0
frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart

@@ -0,0 +1,17 @@
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+
+class TypeOptionService {
+  String fieldId;
+  TypeOptionService({
+    required this.fieldId,
+  });
+
+  Future<Either<SelectOption, FlowyError>> createOption(String name) {
+    final payload = CreateSelectOptionPayload.create()..optionName = name;
+    return GridEventCreateSelectOption(payload).send();
+  }
+}

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

@@ -14,7 +14,7 @@ export 'field/switch_field_type_bloc.dart';
 // Field Type Option
 export 'field/type_option/date_bloc.dart';
 export 'field/type_option/number_bloc.dart';
-export 'field/type_option/selection_bloc.dart';
+export 'field/type_option/single_select_bloc.dart';
 
 // Cell
 export 'cell_bloc/text_cell_bloc.dart';

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart

@@ -61,6 +61,7 @@ class _DocumentPageState extends State<DocumentPage> {
   @override
   Future<void> dispose() async {
     documentBloc.close();
+    _focusNode.dispose();
     super.dispose();
   }
 

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart

@@ -20,7 +20,7 @@ class CreateFieldPannel extends FlowyOverlayDelegate {
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
         child: _CreateFieldPannelWidget(_createFieldBloc),
-        constraints: BoxConstraints.loose(const Size(220, 500)),
+        constraints: BoxConstraints.loose(const Size(220, 400)),
       ),
       identifier: identifier(),
       anchorContext: context,

+ 21 - 10
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart

@@ -2,7 +2,6 @@ import 'dart:typed_data';
 
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart';
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -14,12 +13,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pbserver.dart
 import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart';
 
+import 'type_option/multi_select.dart';
 import 'type_option/number.dart';
+import 'type_option/single_select.dart';
 
 typedef SelectFieldCallback = void Function(Field, Uint8List);
 
@@ -50,7 +50,7 @@ class _FieldTypeSwitcherState extends State<FieldTypeSwitcher> {
 
           final typeOptionWidget = _typeOptionWidget(
             context: context,
-            fieldType: state.field.fieldType,
+            field: state.field,
             data: state.typeOptionData,
           );
 
@@ -89,7 +89,7 @@ class _FieldTypeSwitcherState extends State<FieldTypeSwitcher> {
 
   Widget? _typeOptionWidget({
     required BuildContext context,
-    required FieldType fieldType,
+    required Field field,
     required TypeOptionData data,
   }) {
     final delegate = TypeOptionOperationDelegate(
@@ -97,8 +97,9 @@ class _FieldTypeSwitcherState extends State<FieldTypeSwitcher> {
         context.read<FieldTypeSwitchBloc>().add(FieldTypeSwitchEvent.didUpdateTypeOptionData(data));
       },
       requireToShowOverlay: _showOverlay,
+      hideOverlay: _hideOverlay,
     );
-    final builder = _makeTypeOptionBuild(fieldType: fieldType, data: data, delegate: delegate);
+    final builder = _makeTypeOptionBuild(field: field, data: data, delegate: delegate);
     return builder.customWidget;
   }
 
@@ -120,6 +121,12 @@ class _FieldTypeSwitcherState extends State<FieldTypeSwitcher> {
       anchorOffset: const Offset(-20, 0),
     );
   }
+
+  void _hideOverlay(BuildContext context) {
+    if (currentOverlayIdentifier != null) {
+      FlowyOverlay.of(context).remove(currentOverlayIdentifier!);
+    }
+  }
 }
 
 abstract class TypeOptionBuilder {
@@ -127,23 +134,24 @@ abstract class TypeOptionBuilder {
 }
 
 TypeOptionBuilder _makeTypeOptionBuild({
-  required FieldType fieldType,
+  required Field field,
   required TypeOptionData data,
   required TypeOptionOperationDelegate delegate,
 }) {
-  switch (fieldType) {
+  switch (field.fieldType) {
     case FieldType.Checkbox:
       return CheckboxTypeOptionBuilder(data);
     case FieldType.DateTime:
       return DateTypeOptionBuilder(data, delegate);
+    case FieldType.SingleSelect:
+      return SingleSelectTypeOptionBuilder(field.id, data, delegate);
     case FieldType.MultiSelect:
-      return MultiSelectTypeOptionBuilder(data);
+      return MultiSelectTypeOptionBuilder(data, delegate);
     case FieldType.Number:
       return NumberTypeOptionBuilder(data, delegate);
     case FieldType.RichText:
       return RichTextTypeOptionBuilder(data);
-    case FieldType.SingleSelect:
-      return SingleSelectTypeOptionBuilder(data);
+
     default:
       throw UnimplementedError;
   }
@@ -156,13 +164,16 @@ abstract class TypeOptionWidget extends StatelessWidget {
 typedef TypeOptionData = Uint8List;
 typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData);
 typedef ShowOverlayCallback = void Function(BuildContext anchorContext, String overlayIdentifier, Widget child);
+typedef HideOverlayCallback = void Function(BuildContext anchorContext);
 
 class TypeOptionOperationDelegate {
   TypeOptionDataCallback didUpdateTypeOptionData;
   ShowOverlayCallback requireToShowOverlay;
+  HideOverlayCallback hideOverlay;
   TypeOptionOperationDelegate({
     required this.didUpdateTypeOptionData,
     required this.requireToShowOverlay,
+    required this.hideOverlay,
   });
 }
 

+ 115 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart

@@ -0,0 +1,115 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+
+class SelectOptionColorList extends StatelessWidget {
+  const SelectOptionColorList({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+}
+
+class _SelectOptionColorItem extends StatelessWidget {
+  final SelectOptionColor option;
+  final bool isSelected;
+  const _SelectOptionColorItem({required this.option, required this.isSelected, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    Widget? checkmark;
+    if (isSelected) {
+      checkmark = svg("grid/details", color: theme.iconColor);
+    }
+
+    final colorIcon = SizedBox.square(
+      dimension: 16,
+      child: Container(
+        decoration: BoxDecoration(
+          color: option.color(context),
+          shape: BoxShape.circle,
+        ),
+      ),
+    );
+
+    return FlowyButton(
+      text: FlowyText.medium(
+        option.name(),
+        fontSize: 12,
+      ),
+      hoverColor: theme.hover,
+      leftIcon: colorIcon,
+      rightIcon: checkmark,
+      onTap: () {},
+    );
+  }
+}
+
+enum SelectOptionColor {
+  purple,
+  pink,
+  lightPink,
+  orange,
+  yellow,
+  lime,
+  green,
+  aqua,
+  blue,
+}
+
+extension SelectOptionColorExtension on SelectOptionColor {
+  Color color(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    switch (this) {
+      case SelectOptionColor.purple:
+        return theme.tint1;
+      case SelectOptionColor.pink:
+        return theme.tint2;
+      case SelectOptionColor.lightPink:
+        return theme.tint3;
+      case SelectOptionColor.orange:
+        return theme.tint4;
+      case SelectOptionColor.yellow:
+        return theme.tint5;
+      case SelectOptionColor.lime:
+        return theme.tint6;
+      case SelectOptionColor.green:
+        return theme.tint7;
+      case SelectOptionColor.aqua:
+        return theme.tint8;
+      case SelectOptionColor.blue:
+        return theme.tint9;
+    }
+  }
+
+  String name() {
+    switch (this) {
+      case SelectOptionColor.purple:
+        return LocaleKeys.grid_selectOption_purpleColor.tr();
+      case SelectOptionColor.pink:
+        return LocaleKeys.grid_selectOption_pinkColor.tr();
+      case SelectOptionColor.lightPink:
+        return LocaleKeys.grid_selectOption_lightPinkColor.tr();
+      case SelectOptionColor.orange:
+        return LocaleKeys.grid_selectOption_orangeColor.tr();
+      case SelectOptionColor.yellow:
+        return LocaleKeys.grid_selectOption_yellowColor.tr();
+      case SelectOptionColor.lime:
+        return LocaleKeys.grid_selectOption_limeColor.tr();
+      case SelectOptionColor.green:
+        return LocaleKeys.grid_selectOption_greenColor.tr();
+      case SelectOptionColor.aqua:
+        return LocaleKeys.grid_selectOption_aquaColor.tr();
+      case SelectOptionColor.blue:
+        return LocaleKeys.grid_selectOption_blueColor.tr();
+    }
+  }
+}

+ 48 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart

@@ -0,0 +1,48 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'option_pannel.dart';
+
+class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
+  MultiSelectTypeOption typeOption;
+  TypeOptionOperationDelegate delegate;
+
+  MultiSelectTypeOptionBuilder(TypeOptionData typeOptionData, this.delegate)
+      : typeOption = MultiSelectTypeOption.fromBuffer(typeOptionData);
+
+  @override
+  Widget? get customWidget => MultiSelectTypeOptionWidget(typeOption, delegate);
+}
+
+class MultiSelectTypeOptionWidget extends TypeOptionWidget {
+  final MultiSelectTypeOption typeOption;
+  final TypeOptionOperationDelegate delegate;
+  const MultiSelectTypeOptionWidget(this.typeOption, this.delegate, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => getIt<MultiSelectTypeOptionBloc>(param1: typeOption),
+      child: BlocBuilder<MultiSelectTypeOptionBloc, MultiSelectTypeOptionState>(
+        builder: (context, state) {
+          return OptionPannel(
+            options: state.typeOption.options,
+            beginEdit: () {
+              delegate.hideOverlay(context);
+            },
+            createOptionCallback: (name) {
+              context.read<MultiSelectTypeOptionBloc>().add(MultiSelectTypeOptionEvent.createOption(name));
+            },
+            updateOptionsCallback: (options) {
+              context.read<MultiSelectTypeOptionBloc>().add(MultiSelectTypeOptionEvent.updateOptions(options));
+            },
+          );
+        },
+      ),
+    );
+  }
+}

+ 57 - 80
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart → frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/option_pannel.dart

@@ -1,8 +1,5 @@
-import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/option_pannel_bloc.dart';
-import 'package:app_flowy/workspace/application/grid/field/type_option/selection_bloc.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -16,73 +13,48 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 
 import 'widget.dart';
 
-class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
-  SingleSelectTypeOption typeOption;
-
-  SingleSelectTypeOptionBuilder(TypeOptionData typeOptionData)
-      : typeOption = SingleSelectTypeOption.fromBuffer(typeOptionData);
-
-  @override
-  Widget? get customWidget => SingleSelectTypeOptionWidget(typeOption);
-}
-
-class SingleSelectTypeOptionWidget extends TypeOptionWidget {
-  final SingleSelectTypeOption typeOption;
-  const SingleSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => getIt<SelectionTypeOptionBloc>(),
-      child: OptionPannel(options: typeOption.options),
-    );
-  }
-}
-
-class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
-  MultiSelectTypeOption typeOption;
-
-  MultiSelectTypeOptionBuilder(TypeOptionData typeOptionData)
-      : typeOption = MultiSelectTypeOption.fromBuffer(typeOptionData);
-
-  @override
-  Widget? get customWidget => MultiSelectTypeOptionWidget(typeOption);
-}
-
-class MultiSelectTypeOptionWidget extends TypeOptionWidget {
-  final MultiSelectTypeOption typeOption;
-  const MultiSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => getIt<SelectionTypeOptionBloc>(),
-      child: OptionPannel(options: typeOption.options),
-    );
-  }
-}
-
 class OptionPannel extends StatelessWidget {
   final List<SelectOption> options;
-  const OptionPannel({required this.options, Key? key}) : super(key: key);
+  final VoidCallback beginEdit;
+  final Function(String optionName) createOptionCallback;
+  final Function(List<SelectOption>) updateOptionsCallback;
+  const OptionPannel({
+    required this.options,
+    required this.beginEdit,
+    required this.createOptionCallback,
+    required this.updateOptionsCallback,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => OptionPannelBloc(options: options),
-      child: BlocBuilder<OptionPannelBloc, OptionPannelState>(
+      child: BlocConsumer<OptionPannelBloc, OptionPannelState>(
+        listener: (context, state) {
+          if (state.isEditingOption) {
+            beginEdit();
+          }
+          state.newOptionName.fold(
+            () => null,
+            (optionName) => createOptionCallback(optionName),
+          );
+        },
         builder: (context, state) {
-          List<Widget> children = [const OptionTitle()];
-          if (state.isAddingOption) {
+          List<Widget> children = [
+            const TypeOptionSeparator(),
+            const OptionTitle(),
+          ];
+          if (state.isEditingOption) {
             children.add(const _AddOptionTextField());
           }
 
-          if (state.options.isEmpty && !state.isAddingOption) {
+          if (state.options.isEmpty && !state.isEditingOption) {
             children.add(const _AddOptionButton());
           }
 
           if (state.options.isNotEmpty) {
-            children.add(const _OptionList());
+            children.add(_OptionList(key: ObjectKey(state.options)));
           }
 
           return Column(children: children);
@@ -100,19 +72,27 @@ class OptionTitle extends StatelessWidget {
     final theme = context.watch<AppTheme>();
 
     return BlocBuilder<OptionPannelBloc, OptionPannelState>(
-      buildWhen: (previous, current) => previous.options.length != current.options.length,
       builder: (context, state) {
         List<Widget> children = [FlowyText.medium(LocaleKeys.grid_field_optionTitle.tr(), fontSize: 12)];
-
-        if (state.options.isNotEmpty && state.isAddingOption == false) {
-          children.add(FlowyButton(
-            text: FlowyText.medium(LocaleKeys.grid_field_addOption.tr(), fontSize: 12),
-            hoverColor: theme.hover,
-            onTap: () {
-              context.read<OptionPannelBloc>().add(const OptionPannelEvent.beginAddingOption());
-            },
-            rightIcon: svg("grid/more", color: theme.iconColor),
-          ));
+        if (state.options.isNotEmpty) {
+          children.add(const Spacer());
+          children.add(
+            SizedBox(
+              width: 100,
+              height: 26,
+              child: FlowyButton(
+                text: FlowyText.medium(
+                  LocaleKeys.grid_field_addOption.tr(),
+                  fontSize: 12,
+                  textAlign: TextAlign.center,
+                ),
+                hoverColor: theme.hover,
+                onTap: () {
+                  context.read<OptionPannelBloc>().add(const OptionPannelEvent.beginAddingOption());
+                },
+              ),
+            ),
+          );
         }
 
         return SizedBox(
@@ -135,19 +115,16 @@ class _OptionList extends StatelessWidget {
           return _OptionItem(option: option);
         }).toList();
 
-        return SizedBox(
-          width: 120,
-          child: ListView.separated(
-            shrinkWrap: true,
-            controller: ScrollController(),
-            separatorBuilder: (context, index) {
-              return VSpace(GridSize.typeOptionSeparatorHeight);
-            },
-            itemCount: optionItems.length,
-            itemBuilder: (BuildContext context, int index) {
-              return optionItems[index];
-            },
-          ),
+        return ListView.separated(
+          shrinkWrap: true,
+          controller: ScrollController(),
+          separatorBuilder: (context, index) {
+            return VSpace(GridSize.typeOptionSeparatorHeight);
+          },
+          itemCount: optionItems.length,
+          itemBuilder: (BuildContext context, int index) {
+            return optionItems[index];
+          },
         );
       },
     );
@@ -167,7 +144,7 @@ class _OptionItem extends StatelessWidget {
         text: FlowyText.medium(option.name, fontSize: 12),
         hoverColor: theme.hover,
         onTap: () {},
-        rightIcon: svg("grid/more", color: theme.iconColor),
+        rightIcon: svg("grid/details", color: theme.iconColor),
       ),
     );
   }

+ 55 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart

@@ -0,0 +1,55 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/single_select_bloc.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'option_pannel.dart';
+
+class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
+  final SingleSelectTypeOptionWidget _widget;
+
+  SingleSelectTypeOptionBuilder(
+    String fieldId,
+    TypeOptionData typeOptionData,
+    TypeOptionOperationDelegate delegate,
+  ) : _widget = SingleSelectTypeOptionWidget(
+          fieldId,
+          SingleSelectTypeOption.fromBuffer(typeOptionData),
+          delegate,
+        );
+
+  @override
+  Widget? get customWidget => _widget;
+}
+
+class SingleSelectTypeOptionWidget extends TypeOptionWidget {
+  final String fieldId;
+  final SingleSelectTypeOption typeOption;
+  final TypeOptionOperationDelegate delegate;
+  const SingleSelectTypeOptionWidget(this.fieldId, this.typeOption, this.delegate, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => getIt<SingleSelectTypeOptionBloc>(param1: typeOption, param2: fieldId),
+      child: BlocConsumer<SingleSelectTypeOptionBloc, SingleSelectTypeOptionState>(
+        listener: (context, state) => delegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer()),
+        builder: (context, state) {
+          return OptionPannel(
+            options: state.typeOption.options,
+            beginEdit: () {
+              delegate.hideOverlay(context);
+            },
+            createOptionCallback: (name) {
+              context.read<SingleSelectTypeOptionBloc>().add(SingleSelectTypeOptionEvent.createOption(name));
+            },
+            updateOptionsCallback: (options) {
+              context.read<SingleSelectTypeOptionBloc>().add(SingleSelectTypeOptionEvent.updateOptions(options));
+            },
+          );
+        },
+      ),
+    );
+  }
+}

+ 35 - 12
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart

@@ -21,6 +21,7 @@ class NameTextField extends StatefulWidget {
 
 class _NameTextFieldState extends State<NameTextField> {
   late FocusNode _focusNode;
+  var isEdited = false;
   late TextEditingController _controller;
 
   @override
@@ -35,31 +36,53 @@ class _NameTextFieldState extends State<NameTextField> {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
+
     return RoundedInputField(
-        controller: _controller,
-        focusNode: _focusNode,
-        height: 36,
-        style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
-        normalBorderColor: theme.shader4,
-        errorBorderColor: theme.red,
-        focusBorderColor: theme.main1,
-        cursorColor: theme.main1,
-        onChanged: (text) {
-          print(text);
-        });
+      controller: _controller,
+      focusNode: _focusNode,
+      autoFocus: true,
+      height: 36,
+      style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
+      normalBorderColor: theme.shader4,
+      focusBorderColor: theme.main1,
+      cursorColor: theme.main1,
+      onEditingComplete: () {
+        widget.onDone(_controller.text);
+      },
+    );
   }
 
   @override
   void dispose() {
     _focusNode.removeListener(notifyDidEndEditing);
+    _focusNode.dispose();
     super.dispose();
   }
 
   void notifyDidEndEditing() {
     if (_controller.text.isEmpty) {
-      // widget.onCanceled();
+      if (isEdited) {
+        widget.onCanceled();
+      }
+      isEdited = true;
     } else {
       widget.onDone(_controller.text);
     }
   }
 }
+
+class TypeOptionSeparator extends StatelessWidget {
+  const TypeOptionSeparator({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 6),
+      child: Container(
+        color: theme.shader4,
+        height: 0.25,
+      ),
+    );
+  }
+}

+ 3 - 2
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -15,7 +15,7 @@ class FlowyButton extends StatelessWidget {
     Key? key,
     required this.text,
     this.onTap,
-    this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
+    this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
     this.leftIcon,
     this.rightIcon,
     this.hoverColor = Colors.transparent,
@@ -44,12 +44,13 @@ class FlowyButton extends StatelessWidget {
 
     if (rightIcon != null) {
       children.add(SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
-      children.add(const HSpace(6));
     }
 
     return Padding(
       padding: padding,
       child: Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
         children: children,
       ),
     );

+ 6 - 0
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

@@ -15,6 +15,7 @@ class RoundedInputField extends StatefulWidget {
   final String errorText;
   final TextStyle style;
   final ValueChanged<String>? onChanged;
+  final VoidCallback? onEditingComplete;
   final String? initialValue;
   final EdgeInsets margin;
   final EdgeInsets padding;
@@ -22,6 +23,7 @@ class RoundedInputField extends StatefulWidget {
   final double height;
   final FocusNode? focusNode;
   final TextEditingController? controller;
+  final bool autoFocus;
 
   const RoundedInputField({
     Key? key,
@@ -32,6 +34,7 @@ class RoundedInputField extends StatefulWidget {
     this.obscureIcon,
     this.obscureHideIcon,
     this.onChanged,
+    this.onEditingComplete,
     this.normalBorderColor = Colors.transparent,
     this.errorBorderColor = Colors.transparent,
     this.focusBorderColor,
@@ -43,6 +46,7 @@ class RoundedInputField extends StatefulWidget {
     this.height = 48,
     this.focusNode,
     this.controller,
+    this.autoFocus = false,
   }) : super(key: key);
 
   @override
@@ -78,6 +82,7 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
           controller: widget.controller,
           initialValue: widget.initialValue,
           focusNode: widget.focusNode,
+          autofocus: widget.autoFocus,
           onChanged: (value) {
             inputText = value;
             if (widget.onChanged != null) {
@@ -85,6 +90,7 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
             }
             setState(() {});
           },
+          onEditingComplete: widget.onEditingComplete,
           cursorColor: widget.cursorColor,
           obscureText: obscuteText,
           style: widget.style,

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

@@ -137,6 +137,23 @@ class GridEventCreateEditFieldContext {
     }
 }
 
+class GridEventCreateSelectOption {
+     CreateSelectOptionPayload request;
+     GridEventCreateSelectOption(this.request);
+
+    Future<Either<SelectOption, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.CreateSelectOption.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (okBytes) => left(SelectOption.fromBuffer(okBytes)),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class GridEventCreateRow {
      CreateRowPayload request;
      GridEventCreateRow(this.request);

+ 1 - 0
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart

@@ -4,6 +4,7 @@ import 'package:flowy_sdk/log.dart';
 // ignore: unnecessary_import
 import 'package:flowy_sdk/protobuf/dart-ffi/ffi_response.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-net/event.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-net/network_state.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/event_map.pb.dart';

+ 2 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbenum.dart

@@ -47,6 +47,7 @@ class ErrorCode extends $pb.ProtobufEnum {
   static const ErrorCode RowIdIsEmpty = ErrorCode._(430, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RowIdIsEmpty');
   static const ErrorCode FieldIdIsEmpty = ErrorCode._(440, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'FieldIdIsEmpty');
   static const ErrorCode FieldDoesNotExist = ErrorCode._(441, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'FieldDoesNotExist');
+  static const ErrorCode SelectOptionNameIsEmpty = ErrorCode._(442, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'SelectOptionNameIsEmpty');
   static const ErrorCode TypeOptionDataIsEmpty = ErrorCode._(450, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TypeOptionDataIsEmpty');
   static const ErrorCode InvalidData = ErrorCode._(500, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'InvalidData');
 
@@ -88,6 +89,7 @@ class ErrorCode extends $pb.ProtobufEnum {
     RowIdIsEmpty,
     FieldIdIsEmpty,
     FieldDoesNotExist,
+    SelectOptionNameIsEmpty,
     TypeOptionDataIsEmpty,
     InvalidData,
   ];

+ 2 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbjson.dart

@@ -49,10 +49,11 @@ const ErrorCode$json = const {
     const {'1': 'RowIdIsEmpty', '2': 430},
     const {'1': 'FieldIdIsEmpty', '2': 440},
     const {'1': 'FieldDoesNotExist', '2': 441},
+    const {'1': 'SelectOptionNameIsEmpty', '2': 442},
     const {'1': 'TypeOptionDataIsEmpty', '2': 450},
     const {'1': 'InvalidData', '2': 500},
   ],
 };
 
 /// Descriptor for `ErrorCode`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode('CglFcnJvckNvZGUSDAoISW50ZXJuYWwQABIUChBVc2VyVW5hdXRob3JpemVkEAISEgoOUmVjb3JkTm90Rm91bmQQAxIRCg1Vc2VySWRJc0VtcHR5EAQSGAoUV29ya3NwYWNlTmFtZUludmFsaWQQZBIWChJXb3Jrc3BhY2VJZEludmFsaWQQZRIYChRBcHBDb2xvclN0eWxlSW52YWxpZBBmEhgKFFdvcmtzcGFjZURlc2NUb29Mb25nEGcSGAoUV29ya3NwYWNlTmFtZVRvb0xvbmcQaBIQCgxBcHBJZEludmFsaWQQbhISCg5BcHBOYW1lSW52YWxpZBBvEhMKD1ZpZXdOYW1lSW52YWxpZBB4EhgKFFZpZXdUaHVtYm5haWxJbnZhbGlkEHkSEQoNVmlld0lkSW52YWxpZBB6EhMKD1ZpZXdEZXNjVG9vTG9uZxB7EhMKD1ZpZXdEYXRhSW52YWxpZBB8EhMKD1ZpZXdOYW1lVG9vTG9uZxB9EhEKDENvbm5lY3RFcnJvchDIARIRCgxFbWFpbElzRW1wdHkQrAISFwoSRW1haWxGb3JtYXRJbnZhbGlkEK0CEhcKEkVtYWlsQWxyZWFkeUV4aXN0cxCuAhIUCg9QYXNzd29yZElzRW1wdHkQrwISFAoPUGFzc3dvcmRUb29Mb25nELACEiUKIFBhc3N3b3JkQ29udGFpbnNGb3JiaWRDaGFyYWN0ZXJzELECEhoKFVBhc3N3b3JkRm9ybWF0SW52YWxpZBCyAhIVChBQYXNzd29yZE5vdE1hdGNoELMCEhQKD1VzZXJOYW1lVG9vTG9uZxC0AhInCiJVc2VyTmFtZUNvbnRhaW5Gb3JiaWRkZW5DaGFyYWN0ZXJzELUCEhQKD1VzZXJOYW1lSXNFbXB0eRC2AhISCg1Vc2VySWRJbnZhbGlkELcCEhEKDFVzZXJOb3RFeGlzdBC4AhIQCgtUZXh0VG9vTG9uZxCQAxISCg1HcmlkSWRJc0VtcHR5EJoDEhMKDkJsb2NrSWRJc0VtcHR5EKQDEhEKDFJvd0lkSXNFbXB0eRCuAxITCg5GaWVsZElkSXNFbXB0eRC4AxIWChFGaWVsZERvZXNOb3RFeGlzdBC5AxIaChVUeXBlT3B0aW9uRGF0YUlzRW1wdHkQwgMSEAoLSW52YWxpZERhdGEQ9AM=');
+final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode('CglFcnJvckNvZGUSDAoISW50ZXJuYWwQABIUChBVc2VyVW5hdXRob3JpemVkEAISEgoOUmVjb3JkTm90Rm91bmQQAxIRCg1Vc2VySWRJc0VtcHR5EAQSGAoUV29ya3NwYWNlTmFtZUludmFsaWQQZBIWChJXb3Jrc3BhY2VJZEludmFsaWQQZRIYChRBcHBDb2xvclN0eWxlSW52YWxpZBBmEhgKFFdvcmtzcGFjZURlc2NUb29Mb25nEGcSGAoUV29ya3NwYWNlTmFtZVRvb0xvbmcQaBIQCgxBcHBJZEludmFsaWQQbhISCg5BcHBOYW1lSW52YWxpZBBvEhMKD1ZpZXdOYW1lSW52YWxpZBB4EhgKFFZpZXdUaHVtYm5haWxJbnZhbGlkEHkSEQoNVmlld0lkSW52YWxpZBB6EhMKD1ZpZXdEZXNjVG9vTG9uZxB7EhMKD1ZpZXdEYXRhSW52YWxpZBB8EhMKD1ZpZXdOYW1lVG9vTG9uZxB9EhEKDENvbm5lY3RFcnJvchDIARIRCgxFbWFpbElzRW1wdHkQrAISFwoSRW1haWxGb3JtYXRJbnZhbGlkEK0CEhcKEkVtYWlsQWxyZWFkeUV4aXN0cxCuAhIUCg9QYXNzd29yZElzRW1wdHkQrwISFAoPUGFzc3dvcmRUb29Mb25nELACEiUKIFBhc3N3b3JkQ29udGFpbnNGb3JiaWRDaGFyYWN0ZXJzELECEhoKFVBhc3N3b3JkRm9ybWF0SW52YWxpZBCyAhIVChBQYXNzd29yZE5vdE1hdGNoELMCEhQKD1VzZXJOYW1lVG9vTG9uZxC0AhInCiJVc2VyTmFtZUNvbnRhaW5Gb3JiaWRkZW5DaGFyYWN0ZXJzELUCEhQKD1VzZXJOYW1lSXNFbXB0eRC2AhISCg1Vc2VySWRJbnZhbGlkELcCEhEKDFVzZXJOb3RFeGlzdBC4AhIQCgtUZXh0VG9vTG9uZxCQAxISCg1HcmlkSWRJc0VtcHR5EJoDEhMKDkJsb2NrSWRJc0VtcHR5EKQDEhEKDFJvd0lkSXNFbXB0eRCuAxITCg5GaWVsZElkSXNFbXB0eRC4AxIWChFGaWVsZERvZXNOb3RFeGlzdBC5AxIcChdTZWxlY3RPcHRpb25OYW1lSXNFbXB0eRC6AxIaChVUeXBlT3B0aW9uRGF0YUlzRW1wdHkQwgMSEAoLSW52YWxpZERhdGEQ9AM=');

+ 47 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart

@@ -1536,3 +1536,50 @@ class QueryRowPayload extends $pb.GeneratedMessage {
   void clearRowId() => clearField(3);
 }
 
+class CreateSelectOptionPayload extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateSelectOptionPayload', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'optionName')
+    ..hasRequiredFields = false
+  ;
+
+  CreateSelectOptionPayload._() : super();
+  factory CreateSelectOptionPayload({
+    $core.String? optionName,
+  }) {
+    final _result = create();
+    if (optionName != null) {
+      _result.optionName = optionName;
+    }
+    return _result;
+  }
+  factory CreateSelectOptionPayload.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory CreateSelectOptionPayload.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  CreateSelectOptionPayload clone() => CreateSelectOptionPayload()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  CreateSelectOptionPayload copyWith(void Function(CreateSelectOptionPayload) updates) => super.copyWith((message) => updates(message as CreateSelectOptionPayload)) as CreateSelectOptionPayload; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static CreateSelectOptionPayload create() => CreateSelectOptionPayload._();
+  CreateSelectOptionPayload createEmptyInstance() => create();
+  static $pb.PbList<CreateSelectOptionPayload> createRepeated() => $pb.PbList<CreateSelectOptionPayload>();
+  @$core.pragma('dart2js:noInline')
+  static CreateSelectOptionPayload getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateSelectOptionPayload>(create);
+  static CreateSelectOptionPayload? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get optionName => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set optionName($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasOptionName() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearOptionName() => clearField(1);
+}
+

+ 10 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart

@@ -302,3 +302,13 @@ const QueryRowPayload$json = const {
 
 /// Descriptor for `QueryRowPayload`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List queryRowPayloadDescriptor = $convert.base64Decode('Cg9RdWVyeVJvd1BheWxvYWQSFwoHZ3JpZF9pZBgBIAEoCVIGZ3JpZElkEhkKCGJsb2NrX2lkGAIgASgJUgdibG9ja0lkEhUKBnJvd19pZBgDIAEoCVIFcm93SWQ=');
+@$core.Deprecated('Use createSelectOptionPayloadDescriptor instead')
+const CreateSelectOptionPayload$json = const {
+  '1': 'CreateSelectOptionPayload',
+  '2': const [
+    const {'1': 'option_name', '3': 1, '4': 1, '5': 9, '10': 'optionName'},
+  ],
+};
+
+/// Descriptor for `CreateSelectOptionPayload`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List createSelectOptionPayloadDescriptor = $convert.base64Decode('ChlDcmVhdGVTZWxlY3RPcHRpb25QYXlsb2FkEh8KC29wdGlvbl9uYW1lGAEgASgJUgpvcHRpb25OYW1l');

+ 5 - 3
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart

@@ -18,9 +18,10 @@ class GridEvent extends $pb.ProtobufEnum {
   static const GridEvent DeleteField = GridEvent._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteField');
   static const GridEvent DuplicateField = GridEvent._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DuplicateField');
   static const GridEvent CreateEditFieldContext = GridEvent._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateEditFieldContext');
-  static const GridEvent CreateRow = GridEvent._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateRow');
-  static const GridEvent GetRow = GridEvent._(22, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetRow');
-  static const GridEvent UpdateCell = GridEvent._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateCell');
+  static const GridEvent CreateSelectOption = GridEvent._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateSelectOption');
+  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 UpdateCell = GridEvent._(70, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateCell');
 
   static const $core.List<GridEvent> values = <GridEvent> [
     GetGridData,
@@ -31,6 +32,7 @@ class GridEvent extends $pb.ProtobufEnum {
     DeleteField,
     DuplicateField,
     CreateEditFieldContext,
+    CreateSelectOption,
     CreateRow,
     GetRow,
     UpdateCell,

+ 5 - 4
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart

@@ -20,11 +20,12 @@ const GridEvent$json = const {
     const {'1': 'DeleteField', '2': 13},
     const {'1': 'DuplicateField', '2': 15},
     const {'1': 'CreateEditFieldContext', '2': 16},
-    const {'1': 'CreateRow', '2': 21},
-    const {'1': 'GetRow', '2': 22},
-    const {'1': 'UpdateCell', '2': 30},
+    const {'1': 'CreateSelectOption', '2': 30},
+    const {'1': 'CreateRow', '2': 50},
+    const {'1': 'GetRow', '2': 51},
+    const {'1': 'UpdateCell', '2': 70},
   ],
 };
 
 /// Descriptor for `GridEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEgoORHVwbGljYXRlRmllbGQQDxIaChZDcmVhdGVFZGl0RmllbGRDb250ZXh0EBASDQoJQ3JlYXRlUm93EBUSCgoGR2V0Um93EBYSDgoKVXBkYXRlQ2VsbBAe');
+final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEgoORHVwbGljYXRlRmllbGQQDxIaChZDcmVhdGVFZGl0RmllbGRDb250ZXh0EBASFgoSQ3JlYXRlU2VsZWN0T3B0aW9uEB4SDQoJQ3JlYXRlUm93EDISCgoGR2V0Um93EDMSDgoKVXBkYXRlQ2VsbBBG');

+ 9 - 1
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -1,5 +1,5 @@
 use crate::manager::GridManager;
-use crate::services::field::type_option_data_from_str;
+use crate::services::field::{type_option_data_from_str, SelectOption};
 use flowy_error::FlowyError;
 use flowy_grid_data_model::entities::*;
 use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
@@ -88,6 +88,14 @@ pub(crate) async fn duplicate_field_handler(
     Ok(())
 }
 
+#[tracing::instrument(level = "debug", skip(data), err)]
+pub(crate) async fn create_select_option_handler(
+    data: Data<CreateSelectOptionPayload>,
+) -> DataResult<SelectOption, FlowyError> {
+    let params: CreateSelectOptionParams = data.into_inner().try_into()?;
+    data_result(SelectOption::new(&params.option_name))
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn create_edit_field_context_handler(
     data: Data<CreateEditFieldContextParams>,

+ 7 - 3
frontend/rust-lib/flowy-grid/src/event_map.rs

@@ -16,6 +16,7 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         .event(GridEvent::DeleteField, delete_field_handler)
         .event(GridEvent::DuplicateField, duplicate_field_handler)
         .event(GridEvent::CreateEditFieldContext, create_edit_field_context_handler)
+        .event(GridEvent::CreateSelectOption, create_select_option_handler)
         .event(GridEvent::CreateRow, create_row_handler)
         .event(GridEvent::GetRow, get_row_handler)
         .event(GridEvent::UpdateCell, update_cell_handler);
@@ -50,12 +51,15 @@ pub enum GridEvent {
     #[event(input = "CreateEditFieldContextParams", output = "EditFieldContext")]
     CreateEditFieldContext = 16,
 
+    #[event(input = "CreateSelectOptionPayload", output = "SelectOption")]
+    CreateSelectOption = 30,
+
     #[event(input = "CreateRowPayload", output = "Row")]
-    CreateRow = 21,
+    CreateRow = 50,
 
     #[event(input = "QueryRowPayload", output = "Row")]
-    GetRow = 22,
+    GetRow = 51,
 
     #[event(input = "CellMetaChangeset")]
-    UpdateCell = 30,
+    UpdateCell = 70,
 }

+ 13 - 9
frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs

@@ -33,9 +33,10 @@ pub enum GridEvent {
     DeleteField = 13,
     DuplicateField = 15,
     CreateEditFieldContext = 16,
-    CreateRow = 21,
-    GetRow = 22,
-    UpdateCell = 30,
+    CreateSelectOption = 30,
+    CreateRow = 50,
+    GetRow = 51,
+    UpdateCell = 70,
 }
 
 impl ::protobuf::ProtobufEnum for GridEvent {
@@ -53,9 +54,10 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             13 => ::std::option::Option::Some(GridEvent::DeleteField),
             15 => ::std::option::Option::Some(GridEvent::DuplicateField),
             16 => ::std::option::Option::Some(GridEvent::CreateEditFieldContext),
-            21 => ::std::option::Option::Some(GridEvent::CreateRow),
-            22 => ::std::option::Option::Some(GridEvent::GetRow),
-            30 => ::std::option::Option::Some(GridEvent::UpdateCell),
+            30 => ::std::option::Option::Some(GridEvent::CreateSelectOption),
+            50 => ::std::option::Option::Some(GridEvent::CreateRow),
+            51 => ::std::option::Option::Some(GridEvent::GetRow),
+            70 => ::std::option::Option::Some(GridEvent::UpdateCell),
             _ => ::std::option::Option::None
         }
     }
@@ -70,6 +72,7 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             GridEvent::DeleteField,
             GridEvent::DuplicateField,
             GridEvent::CreateEditFieldContext,
+            GridEvent::CreateSelectOption,
             GridEvent::CreateRow,
             GridEvent::GetRow,
             GridEvent::UpdateCell,
@@ -101,12 +104,13 @@ impl ::protobuf::reflect::ProtobufValue for GridEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fevent_map.proto*\xcc\x01\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
+    \n\x0fevent_map.proto*\xe4\x01\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\x12\n\x0eDuplicateField\x10\x0f\x12\x1a\n\x16CreateE\
-    ditFieldContext\x10\x10\x12\r\n\tCreateRow\x10\x15\x12\n\n\x06GetRow\x10\
-    \x16\x12\x0e\n\nUpdateCell\x10\x1eb\x06proto3\
+    ditFieldContext\x10\x10\x12\x16\n\x12CreateSelectOption\x10\x1e\x12\r\n\
+    \tCreateRow\x102\x12\n\n\x06GetRow\x103\x12\x0e\n\nUpdateCell\x10Fb\x06p\
+    roto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 4 - 3
frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto

@@ -9,7 +9,8 @@ enum GridEvent {
     DeleteField = 13;
     DuplicateField = 15;
     CreateEditFieldContext = 16;
-    CreateRow = 21;
-    GetRow = 22;
-    UpdateCell = 30;
+    CreateSelectOption = 30;
+    CreateRow = 50;
+    GetRow = 51;
+    UpdateCell = 70;
 }

+ 2 - 0
shared-lib/flowy-error-code/src/code.rs

@@ -99,6 +99,8 @@ pub enum ErrorCode {
     FieldIdIsEmpty = 440,
     #[display(fmt = "Field doesn't exist")]
     FieldDoesNotExist = 441,
+    #[display(fmt = "The name of the option should not be empty")]
+    SelectOptionNameIsEmpty = 442,
 
     #[display(fmt = "Field's type option data should not be empty")]
     TypeOptionDataIsEmpty = 450,

+ 7 - 3
shared-lib/flowy-error-code/src/protobuf/model/code.rs

@@ -62,6 +62,7 @@ pub enum ErrorCode {
     RowIdIsEmpty = 430,
     FieldIdIsEmpty = 440,
     FieldDoesNotExist = 441,
+    SelectOptionNameIsEmpty = 442,
     TypeOptionDataIsEmpty = 450,
     InvalidData = 500,
 }
@@ -110,6 +111,7 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
             430 => ::std::option::Option::Some(ErrorCode::RowIdIsEmpty),
             440 => ::std::option::Option::Some(ErrorCode::FieldIdIsEmpty),
             441 => ::std::option::Option::Some(ErrorCode::FieldDoesNotExist),
+            442 => ::std::option::Option::Some(ErrorCode::SelectOptionNameIsEmpty),
             450 => ::std::option::Option::Some(ErrorCode::TypeOptionDataIsEmpty),
             500 => ::std::option::Option::Some(ErrorCode::InvalidData),
             _ => ::std::option::Option::None
@@ -155,6 +157,7 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
             ErrorCode::RowIdIsEmpty,
             ErrorCode::FieldIdIsEmpty,
             ErrorCode::FieldDoesNotExist,
+            ErrorCode::SelectOptionNameIsEmpty,
             ErrorCode::TypeOptionDataIsEmpty,
             ErrorCode::InvalidData,
         ];
@@ -185,7 +188,7 @@ impl ::protobuf::reflect::ProtobufValue for ErrorCode {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\ncode.proto*\x80\x07\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\
+    \n\ncode.proto*\x9e\x07\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\
     \n\x10UserUnauthorized\x10\x02\x12\x12\n\x0eRecordNotFound\x10\x03\x12\
     \x11\n\rUserIdIsEmpty\x10\x04\x12\x18\n\x14WorkspaceNameInvalid\x10d\x12\
     \x16\n\x12WorkspaceIdInvalid\x10e\x12\x18\n\x14AppColorStyleInvalid\x10f\
@@ -205,8 +208,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     erNotExist\x10\xb8\x02\x12\x10\n\x0bTextTooLong\x10\x90\x03\x12\x12\n\rG\
     ridIdIsEmpty\x10\x9a\x03\x12\x13\n\x0eBlockIdIsEmpty\x10\xa4\x03\x12\x11\
     \n\x0cRowIdIsEmpty\x10\xae\x03\x12\x13\n\x0eFieldIdIsEmpty\x10\xb8\x03\
-    \x12\x16\n\x11FieldDoesNotExist\x10\xb9\x03\x12\x1a\n\x15TypeOptionDataI\
-    sEmpty\x10\xc2\x03\x12\x10\n\x0bInvalidData\x10\xf4\x03b\x06proto3\
+    \x12\x16\n\x11FieldDoesNotExist\x10\xb9\x03\x12\x1c\n\x17SelectOptionNam\
+    eIsEmpty\x10\xba\x03\x12\x1a\n\x15TypeOptionDataIsEmpty\x10\xc2\x03\x12\
+    \x10\n\x0bInvalidData\x10\xf4\x03b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 0
shared-lib/flowy-error-code/src/protobuf/proto/code.proto

@@ -38,6 +38,7 @@ enum ErrorCode {
     RowIdIsEmpty = 430;
     FieldIdIsEmpty = 440;
     FieldDoesNotExist = 441;
+    SelectOptionNameIsEmpty = 442;
     TypeOptionDataIsEmpty = 450;
     InvalidData = 500;
 }

+ 22 - 1
shared-lib/flowy-grid-data-model/src/entities/grid.rs

@@ -1,5 +1,5 @@
 use crate::entities::{FieldMeta, FieldType, RowMeta};
-use crate::parser::NotEmptyUuid;
+use crate::parser::{NotEmptyStr, NotEmptyUuid};
 use flowy_derive::ProtoBuf;
 use flowy_error_code::ErrorCode;
 use std::collections::HashMap;
@@ -494,3 +494,24 @@ impl TryInto<QueryRowParams> for QueryRowPayload {
         })
     }
 }
+
+#[derive(ProtoBuf, Default)]
+pub struct CreateSelectOptionPayload {
+    #[pb(index = 1)]
+    pub option_name: String,
+}
+
+pub struct CreateSelectOptionParams {
+    pub option_name: String,
+}
+
+impl TryInto<CreateSelectOptionParams> for CreateSelectOptionPayload {
+    type Error = ErrorCode;
+
+    fn try_into(self) -> Result<CreateSelectOptionParams, Self::Error> {
+        let option_name = NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?;
+        Ok(CreateSelectOptionParams {
+            option_name: option_name.0,
+        })
+    }
+}

+ 2 - 2
shared-lib/flowy-grid-data-model/src/parser/mod.rs

@@ -1,3 +1,3 @@
-mod id_parser;
+mod str_parser;
 
-pub use id_parser::*;
+pub use str_parser::*;

+ 18 - 0
shared-lib/flowy-grid-data-model/src/parser/id_parser.rs → shared-lib/flowy-grid-data-model/src/parser/str_parser.rs

@@ -19,3 +19,21 @@ impl AsRef<str> for NotEmptyUuid {
         &self.0
     }
 }
+
+#[derive(Debug)]
+pub struct NotEmptyStr(pub String);
+
+impl NotEmptyStr {
+    pub fn parse(s: String) -> Result<Self, String> {
+        if s.trim().is_empty() {
+            return Err("Input string is empty".to_owned());
+        }
+        Ok(Self(s))
+    }
+}
+
+impl AsRef<str> for NotEmptyStr {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}

+ 162 - 1
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -5258,6 +5258,165 @@ impl ::protobuf::reflect::ProtobufValue for QueryRowPayload {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct CreateSelectOptionPayload {
+    // message fields
+    pub option_name: ::std::string::String,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a CreateSelectOptionPayload {
+    fn default() -> &'a CreateSelectOptionPayload {
+        <CreateSelectOptionPayload as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl CreateSelectOptionPayload {
+    pub fn new() -> CreateSelectOptionPayload {
+        ::std::default::Default::default()
+    }
+
+    // string option_name = 1;
+
+
+    pub fn get_option_name(&self) -> &str {
+        &self.option_name
+    }
+    pub fn clear_option_name(&mut self) {
+        self.option_name.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_option_name(&mut self, v: ::std::string::String) {
+        self.option_name = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_option_name(&mut self) -> &mut ::std::string::String {
+        &mut self.option_name
+    }
+
+    // Take field
+    pub fn take_option_name(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.option_name, ::std::string::String::new())
+    }
+}
+
+impl ::protobuf::Message for CreateSelectOptionPayload {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.option_name)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.option_name.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.option_name);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.option_name.is_empty() {
+            os.write_string(1, &self.option_name)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> CreateSelectOptionPayload {
+        CreateSelectOptionPayload::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "option_name",
+                |m: &CreateSelectOptionPayload| { &m.option_name },
+                |m: &mut CreateSelectOptionPayload| { &mut m.option_name },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<CreateSelectOptionPayload>(
+                "CreateSelectOptionPayload",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static CreateSelectOptionPayload {
+        static instance: ::protobuf::rt::LazyV2<CreateSelectOptionPayload> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(CreateSelectOptionPayload::new)
+    }
+}
+
+impl ::protobuf::Clear for CreateSelectOptionPayload {
+    fn clear(&mut self) {
+        self.option_name.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for CreateSelectOptionPayload {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for CreateSelectOptionPayload {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\ngrid.proto\x1a\nmeta.proto\"z\n\x04Grid\x12\x0e\n\x02id\x18\x01\x20\
     \x01(\tR\x02id\x12.\n\x0cfield_orders\x18\x02\x20\x03(\x0b2\x0b.FieldOrd\
@@ -5311,7 +5470,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     ridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\x0b2\x0f.GridBlockOrderR\
     \x0bblockOrders\"\\\n\x0fQueryRowPayload\x12\x17\n\x07grid_id\x18\x01\
     \x20\x01(\tR\x06gridId\x12\x19\n\x08block_id\x18\x02\x20\x01(\tR\x07bloc\
-    kId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\x05rowIdb\x06proto3\
+    kId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\x05rowId\"<\n\x19CreateSelec\
+    tOptionPayload\x12\x1f\n\x0boption_name\x18\x01\x20\x01(\tR\noptionNameb\
+    \x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 3 - 0
shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

@@ -103,3 +103,6 @@ message QueryRowPayload {
     string block_id = 2;
     string row_id = 3;
 }
+message CreateSelectOptionPayload {
+    string option_name = 1;
+}