Browse Source

feat: use commas to select a tag in multi-select grid cells (#1158)

* fix: comma to select tags

* chore: support passing multiple option ids

* chore: add more unit tests for single select and multi select

* chore: move to select multiple options using a single payload

* chore: do not unselect the option if insert option ids contain that option

Co-authored-by: appflowy <[email protected]>
Richard Shiue 2 years ago
parent
commit
295b887cf1
17 changed files with 383 additions and 214 deletions
  1. 17 4
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  2. 9 7
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  3. 9 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  4. 40 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  5. 5 5
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  6. 4 4
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  7. 102 63
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs
  8. 86 46
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs
  9. 76 52
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs
  10. 1 1
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  11. 1 1
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  12. 2 2
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  13. 2 2
      frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs
  14. 12 12
      frontend/rust-lib/flowy-grid/src/util.rs
  15. 3 7
      frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs
  16. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs
  17. 12 4
      frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs

+ 17 - 4
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart

@@ -57,6 +57,10 @@ class SelectOptionCellEditorBloc
           trySelectOption: (_TrySelectOption value) {
             _trySelectOption(value.optionName, emit);
           },
+          selectMultipleOptions: (_SelectMultipleOptions value) {
+            _selectMultipleOptions(value.optionNames);
+            _filterOption(value.remainder, emit);
+          },
           filterOption: (_SelectOptionFilter value) {
             _filterOption(value.optionName, emit);
           },
@@ -97,14 +101,14 @@ class SelectOptionCellEditorBloc
     final hasSelected = state.selectedOptions
         .firstWhereOrNull((option) => option.id == optionId);
     if (hasSelected != null) {
-      _selectOptionService.unSelect(optionId: optionId);
+      _selectOptionService.unSelect(optionIds: [optionId]);
     } else {
-      _selectOptionService.select(optionId: optionId);
+      _selectOptionService.select(optionIds: [optionId]);
     }
   }
 
   void _trySelectOption(
-      String optionName, Emitter<SelectOptionEditorState> emit) async {
+      String optionName, Emitter<SelectOptionEditorState> emit) {
     SelectOptionPB? matchingOption;
     bool optionExistsButSelected = false;
 
@@ -126,13 +130,20 @@ class SelectOptionCellEditorBloc
 
     // if there is an unselected matching option, select it
     if (matchingOption != null) {
-      _selectOptionService.select(optionId: matchingOption.id);
+      _selectOptionService.select(optionIds: [matchingOption.id]);
     }
 
     // clear the filter
     emit(state.copyWith(filter: none()));
   }
 
+  void _selectMultipleOptions(List<String> optionNames) {
+    final optionIds = state.options
+        .where((e) => optionNames.contains(e.name))
+        .map((e) => e.id);
+    _selectOptionService.select(optionIds: optionIds);
+  }
+
   void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
     final _MakeOptionResult result =
         _makeOptions(Some(optionName), state.allOptions);
@@ -222,6 +233,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
       _SelectOptionFilter;
   const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
       _TrySelectOption;
+  const factory SelectOptionEditorEvent.selectMultipleOptions(
+      List<String> optionNames, String remainder) = _SelectMultipleOptions;
 }
 
 @freezed

+ 9 - 7
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart

@@ -26,7 +26,7 @@ class SelectOptionService {
               ..fieldId = fieldId
               ..rowId = rowId;
             final payload = SelectOptionChangesetPayloadPB.create()
-              ..insertOption = option
+              ..insertOptions.add(option)
               ..cellIdentifier = cellIdentifier;
             return GridEventUpdateSelectOption(payload).send();
           },
@@ -40,7 +40,7 @@ class SelectOptionService {
     required SelectOptionPB option,
   }) {
     final payload = SelectOptionChangesetPayloadPB.create()
-      ..updateOption = option
+      ..updateOptions.add(option)
       ..cellIdentifier = _cellIdentifier();
     return GridEventUpdateSelectOption(payload).send();
   }
@@ -49,7 +49,7 @@ class SelectOptionService {
     required SelectOptionPB option,
   }) {
     final payload = SelectOptionChangesetPayloadPB.create()
-      ..deleteOption = option
+      ..deleteOptions.add(option)
       ..cellIdentifier = _cellIdentifier();
 
     return GridEventUpdateSelectOption(payload).send();
@@ -64,17 +64,19 @@ class SelectOptionService {
     return GridEventGetSelectOptionCellData(payload).send();
   }
 
-  Future<Either<void, FlowyError>> select({required String optionId}) {
+  Future<Either<void, FlowyError>> select(
+      {required Iterable<String> optionIds}) {
     final payload = SelectOptionCellChangesetPayloadPB.create()
       ..cellIdentifier = _cellIdentifier()
-      ..insertOptionId = optionId;
+      ..insertOptionIds.addAll(optionIds);
     return GridEventUpdateSelectOptionCell(payload).send();
   }
 
-  Future<Either<void, FlowyError>> unSelect({required String optionId}) {
+  Future<Either<void, FlowyError>> unSelect(
+      {required Iterable<String> optionIds}) {
     final payload = SelectOptionCellChangesetPayloadPB.create()
       ..cellIdentifier = _cellIdentifier()
-      ..deleteOptionId = optionId;
+      ..deleteOptionIds.addAll(optionIds);
     return GridEventUpdateSelectOptionCell(payload).send();
   }
 

+ 9 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -149,6 +149,7 @@ class _TextField extends StatelessWidget {
             distanceToText: _editorPanelWidth * 0.7,
             maxLength: 30,
             tagController: _tagController,
+            textSeparators: const [','],
             onClick: () => popoverMutex.close(),
             newText: (text) {
               context
@@ -160,6 +161,14 @@ class _TextField extends StatelessWidget {
                   .read<SelectOptionCellEditorBloc>()
                   .add(SelectOptionEditorEvent.trySelectOption(tagName));
             },
+            onPaste: (tagNames, remainder) {
+              context
+                  .read<SelectOptionCellEditorBloc>()
+                  .add(SelectOptionEditorEvent.selectMultipleOptions(
+                    tagNames,
+                    remainder,
+                  ));
+            },
           ),
         );
       },

+ 40 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart

@@ -17,9 +17,11 @@ class SelectOptionTextField extends StatefulWidget {
   final List<SelectOptionPB> options;
   final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
   final double distanceToText;
+  final List<String> textSeparators;
 
   final Function(String) onSubmitted;
   final Function(String) newText;
+  final Function(List<String>, String) onPaste;
   final VoidCallback? onClick;
   final int? maxLength;
 
@@ -29,7 +31,9 @@ class SelectOptionTextField extends StatefulWidget {
     required this.distanceToText,
     required this.tagController,
     required this.onSubmitted,
+    required this.onPaste,
     required this.newText,
+    required this.textSeparators,
     this.onClick,
     this.maxLength,
     TextEditingController? textController,
@@ -65,7 +69,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
       textfieldTagsController: widget.tagController,
       initialTags: widget.selectedOptionMap.keys.toList(),
       focusNode: focusNode,
-      textSeparators: const [','],
+      textSeparators: widget.textSeparators,
       inputfieldBuilder: (
         BuildContext context,
         editController,
@@ -83,7 +87,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
               if (onChanged != null) {
                 onChanged(text);
               }
-              widget.newText(text);
+              _newText(text, editController);
             },
             onSubmitted: (text) {
               if (onSubmitted != null) {
@@ -121,6 +125,40 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
     );
   }
 
+  void _newText(String text, TextEditingController editingController) {
+    if (text.isEmpty) {
+      widget.newText('');
+      return;
+    }
+
+    final trimmedText = text.trim();
+    List<String> splits = [];
+    String currentString = '';
+
+    // split the string into tokens
+    for (final char in trimmedText.split('')) {
+      if (!widget.textSeparators.contains(char)) {
+        currentString += char;
+        continue;
+      }
+      if (currentString.isNotEmpty) {
+        splits.add(currentString);
+      }
+      currentString = '';
+    }
+    // add the remainder (might be '')
+    splits.add(currentString);
+
+    final submittedOptions =
+        splits.sublist(0, splits.length - 1).map((e) => e.trim()).toList();
+
+    final remainder = splits.elementAt(splits.length - 1).trimLeft();
+    editingController.text = remainder;
+    editingController.selection =
+        TextSelection.collapsed(offset: controller.text.length);
+    widget.onPaste(submittedOptions, remainder);
+  }
+
   Widget? _renderTags(BuildContext context, ScrollController sc) {
     if (widget.selectedOptionMap.isEmpty) {
       return null;

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

@@ -341,19 +341,19 @@ pub(crate) async fn update_select_option_handler(
             let mut cell_content_changeset = None;
             let mut is_changed = None;
 
-            if let Some(option) = changeset.insert_option {
-                cell_content_changeset = Some(SelectOptionCellChangeset::from_insert(&option.id).to_str());
+            for option in changeset.insert_options {
+                cell_content_changeset = Some(SelectOptionCellChangeset::from_insert_option_id(&option.id).to_str());
                 type_option.insert_option(option);
                 is_changed = Some(());
             }
 
-            if let Some(option) = changeset.update_option {
+            for option in changeset.update_options {
                 type_option.insert_option(option);
                 is_changed = Some(());
             }
 
-            if let Some(option) = changeset.delete_option {
-                cell_content_changeset = Some(SelectOptionCellChangeset::from_delete(&option.id).to_str());
+            for option in changeset.delete_options {
+                cell_content_changeset = Some(SelectOptionCellChangeset::from_delete_option_id(&option.id).to_str());
                 type_option.delete_option(option);
                 is_changed = Some(());
             }

+ 4 - 4
frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs

@@ -244,14 +244,14 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi
     CellRevision::new(data)
 }
 
-pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision {
-    let cell_data = SelectOptionCellChangeset::from_insert(&option_id).to_str();
+pub fn insert_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
+    let cell_data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
     let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
     CellRevision::new(data)
 }
 
-pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision {
-    let cell_data = SelectOptionCellChangeset::from_delete(&option_id).to_str();
+pub fn delete_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
+    let cell_data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
     let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
     CellRevision::new(data)
 }

+ 102 - 63
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs

@@ -4,7 +4,7 @@ use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOper
 use crate::services::field::type_options::util::get_cell_data;
 use crate::services::field::{
     make_selected_select_options, BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB,
-    SelectOptionIds, SelectOptionOperation, SelectOptionPB, TypeOptionBuilder, SELECTION_IDS_SEPARATOR,
+    SelectOptionIds, SelectOptionOperation, SelectOptionPB, TypeOptionBuilder,
 };
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
@@ -61,29 +61,32 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
         cell_rev: Option<CellRevision>,
     ) -> Result<String, FlowyError> {
         let content_changeset = changeset.try_into_inner()?;
+
+        let insert_option_ids = content_changeset
+            .insert_option_ids
+            .into_iter()
+            .filter(|insert_option_id| self.options.iter().any(|option| &option.id == insert_option_id))
+            .collect::<Vec<String>>();
+
         let new_cell_data: String;
         match cell_rev {
             None => {
-                new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned());
+                new_cell_data = SelectOptionIds::from(insert_option_ids).to_string();
             }
             Some(cell_rev) => {
                 let cell_data = get_cell_data(&cell_rev);
                 let mut select_ids: SelectOptionIds = cell_data.into();
-                if let Some(insert_option_id) = content_changeset.insert_option_id {
-                    tracing::trace!("Insert multi select option: {}", &insert_option_id);
-                    if select_ids.contains(&insert_option_id) {
-                        select_ids.retain(|id| id != &insert_option_id);
-                    } else {
+                for insert_option_id in insert_option_ids {
+                    if !select_ids.contains(&insert_option_id) {
                         select_ids.push(insert_option_id);
                     }
                 }
 
-                if let Some(delete_option_id) = content_changeset.delete_option_id {
-                    tracing::trace!("Delete multi select option: {}", &delete_option_id);
+                for delete_option_id in content_changeset.delete_option_ids {
                     select_ids.retain(|id| id != &delete_option_id);
                 }
 
-                new_cell_data = select_ids.join(SELECTION_IDS_SEPARATOR);
+                new_cell_data = select_ids.to_string();
                 tracing::trace!("Multi select cell data: {}", &new_cell_data);
             }
         }
@@ -114,22 +117,55 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
 }
 #[cfg(test)]
 mod tests {
-    use crate::entities::FieldType;
     use crate::services::cell::CellDataOperation;
     use crate::services::field::type_options::selection_type_option::*;
     use crate::services::field::FieldBuilder;
     use crate::services::field::{MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB};
-    use flowy_grid_data_model::revision::FieldRevision;
 
     #[test]
-    fn multi_select_test() {
-        let google_option = SelectOptionPB::new("Google");
-        let facebook_option = SelectOptionPB::new("Facebook");
-        let twitter_option = SelectOptionPB::new("Twitter");
+    fn multi_select_insert_multi_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let facebook = SelectOptionPB::new("Facebook");
         let multi_select = MultiSelectTypeOptionBuilder::default()
-            .add_option(google_option.clone())
-            .add_option(facebook_option.clone())
-            .add_option(twitter_option);
+            .add_option(google.clone())
+            .add_option(facebook.clone());
+
+        let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
+        let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+        let option_ids = vec![google.id, facebook.id];
+        let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
+
+        assert_eq!(&*select_option_ids, &option_ids);
+    }
+
+    #[test]
+    fn multi_select_unselect_multi_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let facebook = SelectOptionPB::new("Facebook");
+        let multi_select = MultiSelectTypeOptionBuilder::default()
+            .add_option(google.clone())
+            .add_option(facebook.clone());
+
+        let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
+        let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+        let option_ids = vec![google.id, facebook.id];
+
+        // insert
+        let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
+        assert_eq!(&*select_option_ids, &option_ids);
+
+        // delete
+        let data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
+        assert!(select_option_ids.is_empty());
+    }
+
+    #[test]
+    fn multi_select_insert_single_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let multi_select = MultiSelectTypeOptionBuilder::default().add_option(google.clone());
 
         let field_rev = FieldBuilder::new(multi_select)
             .name("Platform")
@@ -137,52 +173,55 @@ mod tests {
             .build();
 
         let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+        let data = SelectOptionCellChangeset::from_insert_option_id(&google.id).to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
+        assert_eq!(cell_option_ids, google.id);
+    }
 
-        let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR);
-        let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str();
-        let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
-        assert_multi_select_options(
-            cell_data,
-            &type_option,
-            &field_rev,
-            vec![google_option.clone(), facebook_option],
-        );
-
-        let data = SelectOptionCellChangeset::from_insert(&google_option.id).to_str();
-        let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
-        assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
-
-        // Invalid option id
-        let cell_data = type_option
-            .apply_changeset(SelectOptionCellChangeset::from_insert("").to_str().into(), None)
-            .unwrap();
-        assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
-
-        // Invalid option id
-        let cell_data = type_option
-            .apply_changeset(SelectOptionCellChangeset::from_insert("123,456").to_str().into(), None)
-            .unwrap();
-        assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
-
-        // Invalid changeset
-        assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
+    #[test]
+    fn multi_select_insert_non_exist_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let multi_select = MultiSelectTypeOptionBuilder::default();
+        let field_rev = FieldBuilder::new(multi_select)
+            .name("Platform")
+            .visibility(true)
+            .build();
+
+        let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+        let data = SelectOptionCellChangeset::from_insert_option_id(&google.id).to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
+        assert!(cell_option_ids.is_empty());
     }
 
-    fn assert_multi_select_options(
-        cell_data: String,
-        type_option: &MultiSelectTypeOptionPB,
-        field_rev: &FieldRevision,
-        expected: Vec<SelectOptionPB>,
-    ) {
-        let field_type: FieldType = field_rev.ty.into();
-        assert_eq!(
-            expected,
-            type_option
-                .decode_cell_data(cell_data.into(), &field_type, field_rev)
-                .unwrap()
-                .parser::<SelectOptionCellDataParser>()
-                .unwrap()
-                .select_options,
-        );
+    #[test]
+    fn multi_select_insert_invalid_option_id_test() {
+        let google = SelectOptionPB::new("Google");
+        let multi_select = MultiSelectTypeOptionBuilder::default().add_option(google);
+
+        let field_rev = FieldBuilder::new(multi_select)
+            .name("Platform")
+            .visibility(true)
+            .build();
+
+        let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+
+        // empty option id string
+        let data = SelectOptionCellChangeset::from_insert_option_id("").to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
+        assert_eq!(cell_option_ids, "");
+
+        let data = SelectOptionCellChangeset::from_insert_option_id("123,456").to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
+        assert_eq!(cell_option_ids, "");
+    }
+
+    #[test]
+    fn multi_select_invalid_changeset_data_test() {
+        let multi_select = MultiSelectTypeOptionBuilder::default();
+        let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
+        let type_option = MultiSelectTypeOptionPB::from(&field_rev);
+
+        // The type of the changeset should be SelectOptionCellChangeset
+        assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
     }
 }

+ 86 - 46
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs

@@ -76,6 +76,8 @@ pub fn make_selected_select_options(
 }
 
 pub trait SelectOptionOperation: TypeOptionDataFormat + Send + Sync {
+    /// Insert the `SelectOptionPB` into corresponding type option.
+    /// Replace the old value if the option already exists in the option list.
     fn insert_option(&mut self, new_option: SelectOptionPB) {
         let options = self.mut_options();
         if let Some(index) = options
@@ -170,6 +172,11 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB {
     }
 }
 
+/// List of select option ids
+///
+/// Calls [to_string] will return a string consists list of ids,
+/// placing a commas separator between each
+///
 #[derive(Default)]
 pub struct SelectOptionIds(Vec<String>);
 
@@ -193,6 +200,10 @@ impl FromCellString for SelectOptionIds {
 
 impl std::convert::From<String> for SelectOptionIds {
     fn from(s: String) -> Self {
+        if s.is_empty() {
+            return Self(vec![]);
+        }
+
         let ids = s
             .split(SELECTION_IDS_SEPARATOR)
             .map(|id| id.to_string())
@@ -201,7 +212,16 @@ impl std::convert::From<String> for SelectOptionIds {
     }
 }
 
+impl std::convert::From<Vec<String>> for SelectOptionIds {
+    fn from(ids: Vec<String>) -> Self {
+        let ids = ids.into_iter().filter(|id| !id.is_empty()).collect::<Vec<String>>();
+        Self(ids)
+    }
+}
+
 impl ToString for SelectOptionIds {
+    /// Returns a string that consists list of ids, placing a commas
+    /// separator between each
     fn to_string(&self) -> String {
         self.0.join(SELECTION_IDS_SEPARATOR)
     }
@@ -254,24 +274,24 @@ pub struct SelectOptionCellChangesetPayloadPB {
     #[pb(index = 1)]
     pub cell_identifier: GridCellIdPB,
 
-    #[pb(index = 2, one_of)]
-    pub insert_option_id: Option<String>,
+    #[pb(index = 2)]
+    pub insert_option_ids: Vec<String>,
 
-    #[pb(index = 3, one_of)]
-    pub delete_option_id: Option<String>,
+    #[pb(index = 3)]
+    pub delete_option_ids: Vec<String>,
 }
 
 pub struct SelectOptionCellChangesetParams {
     pub cell_identifier: GridCellIdParams,
-    pub insert_option_id: Option<String>,
-    pub delete_option_id: Option<String>,
+    pub insert_option_ids: Vec<String>,
+    pub delete_option_ids: Vec<String>,
 }
 
 impl std::convert::From<SelectOptionCellChangesetParams> for CellChangesetPB {
     fn from(params: SelectOptionCellChangesetParams) -> Self {
         let changeset = SelectOptionCellChangeset {
-            insert_option_id: params.insert_option_id,
-            delete_option_id: params.delete_option_id,
+            insert_option_ids: params.insert_option_ids,
+            delete_option_ids: params.delete_option_ids,
         };
         let content = serde_json::to_string(&changeset).unwrap();
         CellChangesetPB {
@@ -288,36 +308,42 @@ impl TryInto<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPaylo
 
     fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
         let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?;
-        let insert_option_id = match self.insert_option_id {
-            None => None,
-            Some(insert_option_id) => Some(
-                NotEmptyStr::parse(insert_option_id)
-                    .map_err(|_| ErrorCode::OptionIdIsEmpty)?
-                    .0,
-            ),
-        };
+        let insert_option_ids = self
+            .insert_option_ids
+            .into_iter()
+            .flat_map(|option_id| match NotEmptyStr::parse(option_id) {
+                Ok(option_id) => Some(option_id.0),
+                Err(_) => {
+                    tracing::error!("The insert option id should not be empty");
+                    None
+                }
+            })
+            .collect::<Vec<String>>();
 
-        let delete_option_id = match self.delete_option_id {
-            None => None,
-            Some(delete_option_id) => Some(
-                NotEmptyStr::parse(delete_option_id)
-                    .map_err(|_| ErrorCode::OptionIdIsEmpty)?
-                    .0,
-            ),
-        };
+        let delete_option_ids = self
+            .delete_option_ids
+            .into_iter()
+            .flat_map(|option_id| match NotEmptyStr::parse(option_id) {
+                Ok(option_id) => Some(option_id.0),
+                Err(_) => {
+                    tracing::error!("The deleted option id should not be empty");
+                    None
+                }
+            })
+            .collect::<Vec<String>>();
 
         Ok(SelectOptionCellChangesetParams {
             cell_identifier,
-            insert_option_id,
-            delete_option_id,
+            insert_option_ids,
+            delete_option_ids,
         })
     }
 }
 
 #[derive(Clone, Serialize, Deserialize)]
 pub struct SelectOptionCellChangeset {
-    pub insert_option_id: Option<String>,
-    pub delete_option_id: Option<String>,
+    pub insert_option_ids: Vec<String>,
+    pub delete_option_ids: Vec<String>,
 }
 
 impl FromCellChangeset for SelectOptionCellChangeset {
@@ -330,17 +356,31 @@ impl FromCellChangeset for SelectOptionCellChangeset {
 }
 
 impl SelectOptionCellChangeset {
-    pub fn from_insert(option_id: &str) -> Self {
+    pub fn from_insert_option_id(option_id: &str) -> Self {
+        SelectOptionCellChangeset {
+            insert_option_ids: vec![option_id.to_string()],
+            delete_option_ids: vec![],
+        }
+    }
+
+    pub fn from_insert_options(option_ids: Vec<String>) -> Self {
         SelectOptionCellChangeset {
-            insert_option_id: Some(option_id.to_string()),
-            delete_option_id: None,
+            insert_option_ids: option_ids,
+            delete_option_ids: vec![],
         }
     }
 
-    pub fn from_delete(option_id: &str) -> Self {
+    pub fn from_delete_option_id(option_id: &str) -> Self {
         SelectOptionCellChangeset {
-            insert_option_id: None,
-            delete_option_id: Some(option_id.to_string()),
+            insert_option_ids: vec![],
+            delete_option_ids: vec![option_id.to_string()],
+        }
+    }
+
+    pub fn from_delete_options(option_ids: Vec<String>) -> Self {
+        SelectOptionCellChangeset {
+            insert_option_ids: vec![],
+            delete_option_ids: option_ids,
         }
     }
 
@@ -369,21 +409,21 @@ pub struct SelectOptionChangesetPayloadPB {
     #[pb(index = 1)]
     pub cell_identifier: GridCellIdPB,
 
-    #[pb(index = 2, one_of)]
-    pub insert_option: Option<SelectOptionPB>,
+    #[pb(index = 2)]
+    pub insert_options: Vec<SelectOptionPB>,
 
-    #[pb(index = 3, one_of)]
-    pub update_option: Option<SelectOptionPB>,
+    #[pb(index = 3)]
+    pub update_options: Vec<SelectOptionPB>,
 
-    #[pb(index = 4, one_of)]
-    pub delete_option: Option<SelectOptionPB>,
+    #[pb(index = 4)]
+    pub delete_options: Vec<SelectOptionPB>,
 }
 
 pub struct SelectOptionChangeset {
     pub cell_identifier: GridCellIdParams,
-    pub insert_option: Option<SelectOptionPB>,
-    pub update_option: Option<SelectOptionPB>,
-    pub delete_option: Option<SelectOptionPB>,
+    pub insert_options: Vec<SelectOptionPB>,
+    pub update_options: Vec<SelectOptionPB>,
+    pub delete_options: Vec<SelectOptionPB>,
 }
 
 impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayloadPB {
@@ -393,9 +433,9 @@ impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayloadPB {
         let cell_identifier = self.cell_identifier.try_into()?;
         Ok(SelectOptionChangeset {
             cell_identifier,
-            insert_option: self.insert_option,
-            update_option: self.update_option,
-            delete_option: self.delete_option,
+            insert_options: self.insert_options,
+            update_options: self.update_options,
+            delete_options: self.delete_options,
         })
     }
 }

+ 76 - 52
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs

@@ -62,12 +62,24 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
         changeset: CellDataChangeset<SelectOptionCellChangeset>,
         _cell_rev: Option<CellRevision>,
     ) -> Result<String, FlowyError> {
-        let select_option_changeset = changeset.try_into_inner()?;
+        let content_changeset = changeset.try_into_inner()?;
         let new_cell_data: String;
-        if let Some(insert_option_id) = select_option_changeset.insert_option_id {
-            new_cell_data = insert_option_id;
-        } else {
+
+        let mut insert_option_ids = content_changeset
+            .insert_option_ids
+            .into_iter()
+            .filter(|insert_option_id| self.options.iter().any(|option| &option.id == insert_option_id))
+            .collect::<Vec<String>>();
+
+        // In single select, the insert_option_ids should only contain one select option id.
+        // Sometimes, the insert_option_ids may contain list of option ids. For example,
+        // copy/paste a ids string.
+        if insert_option_ids.is_empty() {
             new_cell_data = "".to_string()
+        } else {
+            // Just take the first select option
+            let _ = insert_option_ids.drain(1..);
+            new_cell_data = insert_option_ids.pop().unwrap();
         }
 
         Ok(new_cell_data)
@@ -98,71 +110,83 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
 
 #[cfg(test)]
 mod tests {
-    use crate::entities::FieldType;
     use crate::services::cell::CellDataOperation;
 
     use crate::services::field::type_options::*;
     use crate::services::field::FieldBuilder;
-    use flowy_grid_data_model::revision::FieldRevision;
 
     #[test]
-    fn single_select_test() {
-        let google_option = SelectOptionPB::new("Google");
-        let facebook_option = SelectOptionPB::new("Facebook");
-        let twitter_option = SelectOptionPB::new("Twitter");
+    fn single_select_insert_multi_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let facebook = SelectOptionPB::new("Facebook");
         let single_select = SingleSelectTypeOptionBuilder::default()
-            .add_option(google_option.clone())
-            .add_option(facebook_option.clone())
-            .add_option(twitter_option);
+            .add_option(google.clone())
+            .add_option(facebook.clone());
+
+        let field_rev = FieldBuilder::new(single_select).name("Platform").build();
+        let type_option = SingleSelectTypeOptionPB::from(&field_rev);
+        let option_ids = vec![google.id.clone(), facebook.id];
+        let data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
 
-        let field_rev = FieldBuilder::new(single_select)
-            .name("Platform")
-            .visibility(true)
-            .build();
+        assert_eq!(&*select_option_ids, &vec![google.id]);
+    }
+
+    #[test]
+    fn single_select_unselect_multi_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let facebook = SelectOptionPB::new("Facebook");
+        let single_select = SingleSelectTypeOptionBuilder::default()
+            .add_option(google.clone())
+            .add_option(facebook.clone());
 
+        let field_rev = FieldBuilder::new(single_select).name("Platform").build();
         let type_option = SingleSelectTypeOptionPB::from(&field_rev);
+        let option_ids = vec![google.id.clone(), facebook.id];
 
-        let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR);
-        let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str();
-        let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
-        assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]);
+        // insert
+        let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
+        assert_eq!(&*select_option_ids, &vec![google.id]);
 
-        let data = SelectOptionCellChangeset::from_insert(&google_option.id).to_str();
-        let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
-        assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
+        // delete
+        let data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
+        let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
+        assert!(select_option_ids.is_empty());
+    }
 
-        // Invalid option id
-        let cell_data = type_option
-            .apply_changeset(SelectOptionCellChangeset::from_insert("").to_str().into(), None)
-            .unwrap();
-        assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
+    #[test]
+    fn single_select_insert_non_exist_option_test() {
+        let google = SelectOptionPB::new("Google");
+        let single_select = SingleSelectTypeOptionBuilder::default();
+        let field_rev = FieldBuilder::new(single_select).name("Platform").build();
+        let type_option = SingleSelectTypeOptionPB::from(&field_rev);
 
-        // Invalid option id
-        let cell_data = type_option
-            .apply_changeset(SelectOptionCellChangeset::from_insert("123").to_str().into(), None)
-            .unwrap();
+        let option_ids = vec![google.id];
+        let data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
 
-        assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
+        assert!(cell_option_ids.is_empty());
+    }
 
-        // Invalid changeset
-        assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
+    #[test]
+    fn single_select_insert_invalid_option_id_test() {
+        let single_select = SingleSelectTypeOptionBuilder::default();
+        let field_rev = FieldBuilder::new(single_select).name("Platform").build();
+        let type_option = SingleSelectTypeOptionPB::from(&field_rev);
+
+        let data = SelectOptionCellChangeset::from_insert_option_id("").to_str();
+        let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
+        assert_eq!(cell_option_ids, "");
     }
 
-    fn assert_single_select_options(
-        cell_data: String,
-        type_option: &SingleSelectTypeOptionPB,
-        field_rev: &FieldRevision,
-        expected: Vec<SelectOptionPB>,
-    ) {
-        let field_type: FieldType = field_rev.ty.into();
-        assert_eq!(
-            expected,
-            type_option
-                .decode_cell_data(cell_data.into(), &field_type, field_rev)
-                .unwrap()
-                .parser::<SelectOptionCellDataParser>()
-                .unwrap()
-                .select_options,
-        );
+    #[test]
+    fn single_select_invalid_changeset_data_test() {
+        let single_select = SingleSelectTypeOptionBuilder::default();
+        let field_rev = FieldBuilder::new(single_select).name("Platform").build();
+        let type_option = SingleSelectTypeOptionPB::from(&field_rev);
+
+        // The type of the changeset should be SelectOptionCellChangeset
+        assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
     }
 }

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs

@@ -62,7 +62,7 @@ impl GroupController for MultiSelectGroupController {
         match self.group_ctx.get_group(group_id) {
             None => tracing::warn!("Can not find the group: {}", group_id),
             Some((_, group)) => {
-                let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
+                let cell_rev = insert_select_option_cell(vec![group.id.clone()], field_rev);
                 row_rev.cells.insert(field_rev.id.clone(), cell_rev);
             }
         }

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs

@@ -63,7 +63,7 @@ impl GroupController for SingleSelectGroupController {
         match group {
             None => {}
             Some(group) => {
-                let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
+                let cell_rev = insert_select_option_cell(vec![group.id.clone()], field_rev);
                 row_rev.cells.insert(field_rev.id.clone(), cell_rev);
             }
         }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs

@@ -134,11 +134,11 @@ pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Opti
     let field_type: FieldType = field_rev.ty.into();
     match field_type {
         FieldType::SingleSelect => {
-            let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
+            let cell_rev = insert_select_option_cell(vec![group_id.to_owned()], field_rev);
             Some(cell_rev)
         }
         FieldType::MultiSelect => {
-            let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
+            let cell_rev = insert_select_option_cell(vec![group_id.to_owned()], field_rev);
             Some(cell_rev)
         }
         FieldType::Checkbox => {

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs

@@ -92,13 +92,13 @@ impl<'a> RowRevisionBuilder<'a> {
         }
     }
 
-    pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) {
+    pub fn insert_select_option_cell(&mut self, field_id: &str, option_ids: Vec<String>) {
         match self.field_rev_map.get(&field_id.to_owned()) {
             None => tracing::warn!("Can't find the select option field with id: {}", field_id),
             Some(field_rev) => {
                 self.payload
                     .cell_by_field_id
-                    .insert(field_id.to_owned(), insert_select_option_cell(data, field_rev));
+                    .insert(field_id.to_owned(), insert_select_option_cell(option_ids, field_rev));
             }
         }
     }

+ 12 - 12
frontend/rust-lib/flowy-grid/src/util.rs

@@ -60,7 +60,7 @@ pub fn make_default_board() -> BuildGridContext {
 
     for i in 0..3 {
         let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
-        row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone());
+        row_builder.insert_select_option_cell(&single_select_field_id, vec![to_do_option.id.clone()]);
         let data = format!("Card {}", i + 1);
         row_builder.insert_text_cell(&text_field_id, data);
         let row = row_builder.build();
@@ -116,23 +116,23 @@ pub fn make_default_board_2() -> BuildGridContext {
 
     for i in 0..3 {
         let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
-        row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone());
+        row_builder.insert_select_option_cell(&single_select_field_id, vec![to_do_option.id.clone()]);
         match i {
             0 => {
                 row_builder.insert_text_cell(&text_field_id, "Update AppFlowy Website".to_string());
-                row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![work_option.id.clone()]);
             }
             1 => {
                 row_builder.insert_text_cell(&text_field_id, "Learn French".to_string());
                 let mut options = SelectOptionIds::new();
                 options.push(fun_option.id.clone());
                 options.push(travel_option.id.clone());
-                row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
             }
 
             2 => {
                 row_builder.insert_text_cell(&text_field_id, "Exercise 4x/week".to_string());
-                row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![fun_option.id.clone()]);
             }
             _ => {}
         }
@@ -142,15 +142,15 @@ pub fn make_default_board_2() -> BuildGridContext {
 
     for i in 0..3 {
         let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
-        row_builder.insert_select_option_cell(&single_select_field_id, doing_option.id.clone());
+        row_builder.insert_select_option_cell(&single_select_field_id, vec![doing_option.id.clone()]);
         match i {
             0 => {
                 row_builder.insert_text_cell(&text_field_id, "Learn how to swim".to_string());
-                row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![fun_option.id.clone()]);
             }
             1 => {
                 row_builder.insert_text_cell(&text_field_id, "Meditate 10 mins each day".to_string());
-                row_builder.insert_select_option_cell(&multi_select_field_id, health_option.id.clone());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![health_option.id.clone()]);
             }
 
             2 => {
@@ -158,7 +158,7 @@ pub fn make_default_board_2() -> BuildGridContext {
                 let mut options = SelectOptionIds::new();
                 options.push(fun_option.id.clone());
                 options.push(work_option.id.clone());
-                row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
             }
             _ => {}
         }
@@ -168,18 +168,18 @@ pub fn make_default_board_2() -> BuildGridContext {
 
     for i in 0..2 {
         let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
-        row_builder.insert_select_option_cell(&single_select_field_id, done_option.id.clone());
+        row_builder.insert_select_option_cell(&single_select_field_id, vec![done_option.id.clone()]);
         match i {
             0 => {
                 row_builder.insert_text_cell(&text_field_id, "Publish an article".to_string());
-                row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![work_option.id.clone()]);
             }
             1 => {
                 row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string());
                 let mut options = SelectOptionIds::new();
                 options.push(travel_option.id.clone());
                 options.push(fun_option.id.clone());
-                row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
+                row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
             }
 
             _ => {}

+ 3 - 7
frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs

@@ -2,7 +2,7 @@ use flowy_grid::entities::FieldType;
 use std::sync::Arc;
 
 use flowy_grid::services::field::{
-    DateCellChangesetPB, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB, SELECTION_IDS_SEPARATOR,
+    DateCellChangesetPB, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB,
 };
 use flowy_grid::services::row::RowRevisionBuilder;
 use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
@@ -70,7 +70,7 @@ impl<'a> GridRowTestBuilder<'a> {
         let type_option = SingleSelectTypeOptionPB::from(&single_select_field);
         let option = f(type_option.options);
         self.inner_builder
-            .insert_select_option_cell(&single_select_field.id, option.id);
+            .insert_select_option_cell(&single_select_field.id, vec![option.id]);
 
         single_select_field.id.clone()
     }
@@ -82,11 +82,7 @@ impl<'a> GridRowTestBuilder<'a> {
         let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect);
         let type_option = MultiSelectTypeOptionPB::from(&multi_select_field);
         let options = f(type_option.options);
-        let ops_ids = options
-            .iter()
-            .map(|option| option.id.clone())
-            .collect::<Vec<_>>()
-            .join(SELECTION_IDS_SEPARATOR);
+        let ops_ids = options.iter().map(|option| option.id.clone()).collect::<Vec<_>>();
         self.inner_builder
             .insert_select_option_cell(&multi_select_field.id, ops_ids);
 

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs

@@ -25,11 +25,11 @@ async fn grid_cell_update() {
                 FieldType::DateTime => make_date_cell_string("123"),
                 FieldType::SingleSelect => {
                     let type_option = SingleSelectTypeOptionPB::from(field_rev);
-                    SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
+                    SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id).to_str()
                 }
                 FieldType::MultiSelect => {
                     let type_option = MultiSelectTypeOptionPB::from(field_rev);
-                    SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
+                    SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id).to_str()
                 }
                 FieldType::Checkbox => "1".to_string(),
                 FieldType::URL => "1".to_string(),

+ 12 - 4
frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs

@@ -136,16 +136,24 @@ impl GridGroupTest {
 
                 let cell_rev = if to_group.is_default {
                     match field_type {
-                        FieldType::SingleSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev),
-                        FieldType::MultiSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev),
+                        FieldType::SingleSelect => {
+                            delete_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
+                        }
+                        FieldType::MultiSelect => {
+                            delete_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
+                        }
                         _ => {
                             panic!("Unsupported group field type");
                         }
                     }
                 } else {
                     match field_type {
-                        FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev),
-                        FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev),
+                        FieldType::SingleSelect => {
+                            insert_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
+                        }
+                        FieldType::MultiSelect => {
+                            insert_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
+                        }
                         _ => {
                             panic!("Unsupported group field type");
                         }