Browse Source

chore: Update row when moving row caused cell data changed

appflowy 2 years ago
parent
commit
4856a024a2

+ 2 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart

@@ -1,8 +1,10 @@
 import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_svg/svg.dart';
 
 class BoardTextCell extends StatefulWidget {
   final GridCellControllerBuilder cellControllerBuilder;

+ 22 - 9
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -84,20 +84,28 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
     Log.debug(
         '[$AFBoardColumnDataController] $columnData insert $item at $index');
 
-    if (columnData._items.length > index) {
-      columnData._items.insert(index, item);
+    if (_containsItem(item)) {
+      return false;
     } else {
-      columnData._items.add(item);
+      if (columnData._items.length > index) {
+        columnData._items.insert(index, item);
+      } else {
+        columnData._items.add(item);
+      }
+
+      if (notify) notifyListeners();
+      return true;
     }
-
-    if (notify) notifyListeners();
-    return true;
   }
 
   bool add(AFColumnItem item, {bool notify = true}) {
-    columnData._items.add(item);
-    if (notify) notifyListeners();
-    return true;
+    if (_containsItem(item)) {
+      return false;
+    } else {
+      columnData._items.add(item);
+      if (notify) notifyListeners();
+      return true;
+    }
   }
 
   /// Replace the item at index with the [newItem].
@@ -114,6 +122,11 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
 
     notifyListeners();
   }
+
+  bool _containsItem(AFColumnItem item) {
+    return columnData._items.indexWhere((element) => element.id == item.id) !=
+        -1;
+  }
 }
 
 /// [AFBoardColumnData] represents the data of each Column of the Board.

+ 3 - 3
frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs

@@ -1,7 +1,7 @@
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
-use flowy_grid_data_model::revision::{CellRevision, RowMetaChangeset};
+use flowy_grid_data_model::revision::{CellRevision, RowChangeset};
 use std::collections::HashMap;
 
 #[derive(ProtoBuf, Default)]
@@ -135,7 +135,7 @@ pub struct CellChangesetPB {
     pub content: Option<String>,
 }
 
-impl std::convert::From<CellChangesetPB> for RowMetaChangeset {
+impl std::convert::From<CellChangesetPB> for RowChangeset {
     fn from(changeset: CellChangesetPB) -> Self {
         let mut cell_by_field_id = HashMap::with_capacity(1);
         let field_id = changeset.field_id;
@@ -144,7 +144,7 @@ impl std::convert::From<CellChangesetPB> for RowMetaChangeset {
         };
         cell_by_field_id.insert(field_id, cell_rev);
 
-        RowMetaChangeset {
+        RowChangeset {
             row_id: changeset.row_id,
             height: None,
             visibility: None,

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

@@ -1,7 +1,7 @@
 use crate::entities::RowPB;
 use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision};
+use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowChangeset, RowRevision};
 use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder};
 use flowy_sync::client_grid::{GridBlockRevisionChangeset, GridBlockRevisionPad};
 use flowy_sync::entities::revision::Revision;
@@ -88,7 +88,7 @@ impl GridBlockRevisionEditor {
         Ok(row_count)
     }
 
-    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let _ = self.modify(|block_pad| Ok(block_pad.update_row(changeset)?)).await?;
         Ok(())
     }

+ 7 - 11
frontend/rust-lib/flowy-grid/src/services/block_manager.rs

@@ -7,7 +7,7 @@ use crate::services::row::{block_from_row_orders, make_row_from_row_rev, GridBlo
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
-    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
+    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowChangeset, RowRevision,
 };
 use flowy_revision::disk::SQLiteGridBlockRevisionPersistence;
 use flowy_revision::{RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence};
@@ -103,17 +103,15 @@ impl GridBlockManager {
         Ok(changesets)
     }
 
-    pub async fn update_row<F>(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()>
-    where
-        F: FnOnce(Arc<RowRevision>) -> RowPB,
-    {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
         let _ = editor.update_row(changeset.clone()).await?;
         match editor.get_row_rev(&changeset.row_id).await? {
             None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id),
             Some(row_rev) => {
+                let row_pb = make_row_from_row_rev(row_rev.clone());
                 let block_order_changeset =
-                    GridBlockChangesetPB::update(&editor.block_id, vec![row_builder(row_rev.clone())]);
+                    GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]);
                 let _ = self
                     .notify_did_update_block(&editor.block_id, block_order_changeset)
                     .await?;
@@ -191,12 +189,10 @@ impl GridBlockManager {
         }
     }
 
-    pub async fn update_cell<F>(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()>
-    where
-        F: FnOnce(Arc<RowRevision>) -> RowPB,
+    pub async fn update_cell(&self, changeset: CellChangesetPB ) -> FlowyResult<()>
     {
-        let row_changeset: RowMetaChangeset = changeset.clone().into();
-        let _ = self.update_row(row_changeset, row_builder).await?;
+        let row_changeset: RowChangeset = changeset.clone().into();
+        let _ = self.update_row(row_changeset, ).await?;
         self.notify_did_update_cell(changeset).await?;
         Ok(())
     }

+ 16 - 12
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -314,9 +314,9 @@ impl GridRevisionEditor {
         Ok(row_orders)
     }
 
-    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let row_id = changeset.row_id.clone();
-        let _ = self.block_manager.update_row(changeset, make_row_from_row_rev).await?;
+        let _ = self.block_manager.update_row(changeset).await?;
         self.view_manager.did_update_row(&row_id).await;
         Ok(())
     }
@@ -395,12 +395,11 @@ impl GridRevisionEditor {
 
         match self.grid_pad.read().await.get_field_rev(&field_id) {
             None => {
-                let msg = format!("Field not found with id: {}", &field_id);
+                let msg = format!("Field:{} not found", &field_id);
                 Err(FlowyError::internal().context(msg))
             }
             Some((_, field_rev)) => {
                 tracing::trace!("field changeset: id:{} / value:{:?}", &field_id, content);
-
                 let cell_rev = self.get_cell_rev(&row_id, &field_id).await?;
                 // Update the changeset.data property with the return value.
                 content = Some(apply_cell_data_changeset(content.unwrap(), cell_rev, field_rev)?);
@@ -410,11 +409,7 @@ impl GridRevisionEditor {
                     field_id,
                     content,
                 };
-                let _ = self
-                    .block_manager
-                    .update_cell(cell_changeset, make_row_from_row_rev)
-                    .await?;
-
+                let _ = self.block_manager.update_cell(cell_changeset).await?;
                 self.view_manager.did_update_row(&row_id).await;
                 Ok(())
             }
@@ -512,10 +507,19 @@ impl GridRevisionEditor {
                             .block_manager
                             .move_row(row_rev.clone(), from_index, to_index)
                             .await?;
-                        self.view_manager.move_row(row_rev, to_row_id.clone()).await;
+
+                        if let Some(row_changeset) = self.view_manager.move_row(row_rev, to_row_id.clone()).await {
+                            tracing::trace!("Receive row changeset after moving the row");
+                            match self.block_manager.update_row(row_changeset).await {
+                                Ok(_) => {}
+                                Err(e) => {
+                                    tracing::error!("Apply row changeset error:{:?}", e);
+                                }
+                            }
+                        }
                     }
-                    (_, None) => tracing::error!("Can not find the from row id: {}", from_row_id),
-                    (None, _) => tracing::error!("Can not find the to row id: {}", to_row_id),
+                    (_, None) => tracing::warn!("Can not find the from row id: {}", from_row_id),
+                    (None, _) => tracing::warn!("Can not find the to row id: {}", to_row_id),
                 }
             }
         }

+ 3 - 3
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -7,7 +7,7 @@ use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDeleg
 use crate::services::group::{default_group_configuration, GroupConfigurationDelegate, GroupService};
 use crate::services::setting::make_grid_setting;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision};
 use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder};
 use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
 use flowy_sync::entities::grid::GridSettingChangesetParams;
@@ -117,12 +117,12 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn did_move_row(&self, row_rev: &RowRevision, upper_row_id: &str) {
+    pub(crate) async fn did_move_row(&self, row_rev: &RowRevision, row_changeset: &mut RowChangeset, upper_row_id: &str) {
         if let Some(changesets) = self
             .group_service
             .write()
             .await
-            .did_move_row(row_rev, upper_row_id, |field_id| {
+            .did_move_row(row_rev, row_changeset, upper_row_id, |field_id| {
                 self.field_delegate.get_field_rev(&field_id)
             })
             .await

+ 17 - 6
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

@@ -1,13 +1,11 @@
-use crate::entities::{
-    CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB,
-};
+use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB};
 use crate::manager::GridUser;
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
 use crate::services::grid_view_editor::GridViewRevisionEditor;
 use bytes::Bytes;
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 use flowy_revision::disk::SQLiteGridViewRevisionPersistence;
 use flowy_revision::{RevisionCompactor, RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence};
 use flowy_sync::entities::grid::GridSettingChangesetParams;
@@ -56,18 +54,21 @@ impl GridViewManager {
         })
     }
 
+    /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
     pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
             view_editor.will_create_row(row_rev, params).await;
         }
     }
 
+    /// Notify the view that the row was created. For the moment, the view is just sending notifications.
     pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
             view_editor.did_create_row(row_pb, params).await;
         }
     }
 
+    /// Insert/Delete the group's row if the corresponding data was changed.  
     pub(crate) async fn did_update_row(&self, row_id: &str) {
         match self.row_delegate.gv_get_row_rev(row_id).await {
             None => {
@@ -109,9 +110,19 @@ impl GridViewManager {
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
-    pub(crate) async fn move_row(&self, row_rev: Arc<RowRevision>, to_row_id: String) {
+    /// It may generate a RowChangeset when the Row was moved from one group to another.
+    /// The return value, [RowChangeset], contains the changes made by the groups.
+    ///
+    pub(crate) async fn move_row(&self, row_rev: Arc<RowRevision>, to_row_id: String) -> Option<RowChangeset> {
+        let mut row_changeset = RowChangeset::new(row_rev.id.clone());
         for view_editor in self.view_editors.iter() {
-            view_editor.did_move_row(&row_rev, &to_row_id).await;
+            view_editor.did_move_row(&row_rev, &mut row_changeset, &to_row_id).await;
+        }
+
+        if row_changeset.has_changed() {
+            Some(row_changeset)
+        } else {
+            None
         }
     }
 

+ 16 - 3
frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs

@@ -1,6 +1,6 @@
 use crate::entities::{CheckboxGroupConfigurationPB, GroupRowsChangesetPB};
 
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 
 use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
 use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable};
@@ -37,7 +37,9 @@ impl Groupable for CheckboxGroupController {
 
     fn move_row_if_match(
         &mut self,
+        field_rev: &FieldRevision,
         _row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         _cell_data: &Self::CellDataType,
         _to_row_id: &str,
     ) -> Vec<GroupRowsChangesetPB> {
@@ -57,11 +59,22 @@ impl GroupGenerator for CheckboxGroupGenerator {
     type TypeOptionType = CheckboxTypeOptionPB;
 
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         _type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
-        let check_group = Group::new("true".to_string(), "".to_string(), CHECK.to_string());
-        let uncheck_group = Group::new("false".to_string(), "".to_string(), UNCHECK.to_string());
+        let check_group = Group::new(
+            "true".to_string(),
+            field_id.to_owned(),
+            "".to_string(),
+            CHECK.to_string(),
+        );
+        let uncheck_group = Group::new(
+            "false".to_string(),
+            field_id.to_owned(),
+            "".to_string(),
+            UNCHECK.to_string(),
+        );
         vec![check_group, uncheck_group]
     }
 }

+ 12 - 47
frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs

@@ -3,7 +3,7 @@ use crate::services::cell::{decode_any_cell_data, CellBytesParser};
 use bytes::Bytes;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
-    FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer,
+    FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer,
 };
 use indexmap::IndexMap;
 use std::marker::PhantomData;
@@ -14,6 +14,7 @@ pub trait GroupGenerator {
     type TypeOptionType;
 
     fn generate_groups(
+        field_id: &str,
         configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group>;
@@ -31,7 +32,9 @@ pub trait Groupable: Send + Sync {
 
     fn move_row_if_match(
         &mut self,
+        field_rev: &FieldRevision,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         cell_data: &Self::CellDataType,
         to_row_id: &str,
     ) -> Vec<GroupRowsChangesetPB>;
@@ -61,6 +64,7 @@ pub trait GroupControllerSharedAction: Send + Sync {
     fn did_move_row(
         &mut self,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         field_rev: &FieldRevision,
         to_row_id: &str,
     ) -> FlowyResult<Vec<GroupRowsChangesetPB>>;
@@ -85,6 +89,7 @@ pub struct GenericGroupController<C, T, G, P> {
 #[derive(Clone)]
 pub struct Group {
     pub id: String,
+    pub field_id: String,
     pub desc: String,
     rows: Vec<RowPB>,
     pub content: String,
@@ -101,9 +106,10 @@ impl std::convert::From<Group> for GroupPB {
 }
 
 impl Group {
-    pub fn new(id: String, desc: String, content: String) -> Self {
+    pub fn new(id: String, field_id: String, desc: String, content: String) -> Self {
         Self {
             id,
+            field_id,
             desc,
             rows: vec![],
             content,
@@ -158,10 +164,11 @@ where
         };
         let field_type_rev = field_rev.field_type_rev;
         let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
-        let groups = G::generate_groups(&configuration, &type_option);
+        let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
 
         let default_group = Group::new(
             DEFAULT_GROUP_ID.to_owned(),
+            field_rev.id.clone(),
             format!("No {}", field_rev.name),
             "".to_string(),
         );
@@ -263,62 +270,20 @@ where
     fn did_move_row(
         &mut self,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         field_rev: &FieldRevision,
         to_row_id: &str,
     ) -> FlowyResult<Vec<GroupRowsChangesetPB>> {
         if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
             let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
             let cell_data = cell_bytes.parser::<P>()?;
-            Ok(self.move_row_if_match(row_rev, &cell_data, to_row_id))
+            Ok(self.move_row_if_match(field_rev, row_rev, row_changeset, &cell_data, to_row_id))
         } else {
             Ok(vec![])
         }
     }
 }
 
-// impl<C, T, G, P> GroupController<C, T, G, P>
-// where
-//     P: CellBytesParser,
-//     Self: Groupable<CellDataType = P::Object>,
-// {
-//     pub fn handle_rows(&mut self, rows: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
-//         // The field_rev might be None if corresponding field_rev is deleted.
-//         if self.configuration.is_none() {
-//             return Ok(());
-//         }
-//
-//         for row in rows {
-//             if let Some(cell_rev) = row.cells.get(&self.field_id) {
-//                 let mut records: Vec<GroupRecord> = vec![];
-//                 let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
-//                 let cell_data = cell_bytes.parser::<P>()?;
-//                 for group in self.groups_map.values() {
-//                     if self.can_group(&group.content, &cell_data) {
-//                         records.push(GroupRecord {
-//                             row: row.into(),
-//                             group_id: group.id.clone(),
-//                         });
-//                     }
-//                 }
-//
-//                 if records.is_empty() {
-//                     self.default_group.rows.push(row.into());
-//                 } else {
-//                     for record in records {
-//                         if let Some(group) = self.groups_map.get_mut(&record.group_id) {
-//                             group.rows.push(record.row);
-//                         }
-//                     }
-//                 }
-//             } else {
-//                 self.default_group.rows.push(row.into());
-//             }
-//         }
-//
-//         Ok(())
-//     }
-// }
-
 struct GroupRecord {
     row: RowPB,
     group_id: String,

+ 56 - 15
frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs

@@ -5,7 +5,7 @@ use crate::services::field::{
 };
 use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable};
 
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 
 // SingleSelect
 pub type SingleSelectGroupController = GenericGroupController<
@@ -43,15 +43,25 @@ impl Groupable for SingleSelectGroupController {
 
     fn move_row_if_match(
         &mut self,
+        field_rev: &FieldRevision,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         cell_data: &Self::CellDataType,
         to_row_id: &str,
     ) -> Vec<GroupRowsChangesetPB> {
-        let mut changesets = vec![];
+        let mut group_changeset = vec![];
         self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
-            move_row(group, &mut changesets, cell_data, row_rev, to_row_id);
+            move_row(
+                group,
+                &mut group_changeset,
+                field_rev,
+                row_rev,
+                row_changeset,
+                cell_data,
+                to_row_id,
+            );
         });
-        changesets
+        group_changeset
     }
 }
 
@@ -74,6 +84,7 @@ impl GroupGenerator for SingleSelectGroupGenerator {
     type ConfigurationType = SelectOptionGroupConfigurationPB;
     type TypeOptionType = SingleSelectTypeOptionPB;
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
@@ -82,7 +93,14 @@ impl GroupGenerator for SingleSelectGroupGenerator {
             Some(type_option) => type_option
                 .options
                 .iter()
-                .map(|option| Group::new(option.id.clone(), option.name.clone(), option.id.clone()))
+                .map(|option| {
+                    Group::new(
+                        option.id.clone(),
+                        field_id.to_owned(),
+                        option.name.clone(),
+                        option.id.clone(),
+                    )
+                })
                 .collect(),
         }
     }
@@ -125,15 +143,25 @@ impl Groupable for MultiSelectGroupController {
 
     fn move_row_if_match(
         &mut self,
+        field_rev: &FieldRevision,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         cell_data: &Self::CellDataType,
         to_row_id: &str,
     ) -> Vec<GroupRowsChangesetPB> {
-        let mut changesets = vec![];
+        let mut group_changeset = vec![];
         self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
-            move_row(group, &mut changesets, cell_data, row_rev, to_row_id);
+            move_row(
+                group,
+                &mut group_changeset,
+                field_rev,
+                row_rev,
+                row_changeset,
+                cell_data,
+                to_row_id,
+            );
         });
-        changesets
+        group_changeset
     }
 }
 
@@ -156,6 +184,7 @@ impl GroupGenerator for MultiSelectGroupGenerator {
     type TypeOptionType = MultiSelectTypeOptionPB;
 
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
@@ -164,7 +193,14 @@ impl GroupGenerator for MultiSelectGroupGenerator {
             Some(type_option) => type_option
                 .options
                 .iter()
-                .map(|option| Group::new(option.id.clone(), option.name.clone(), option.id.clone()))
+                .map(|option| {
+                    Group::new(
+                        option.id.clone(),
+                        field_id.to_owned(),
+                        option.name.clone(),
+                        option.id.clone(),
+                    )
+                })
                 .collect(),
         }
     }
@@ -204,25 +240,30 @@ fn remove_row(
 
 fn move_row(
     group: &mut Group,
-    changesets: &mut Vec<GroupRowsChangesetPB>,
-    cell_data: &SelectOptionCellDataPB,
+    group_changeset: &mut Vec<GroupRowsChangesetPB>,
+    field_rev: &FieldRevision,
     row_rev: &RowRevision,
-    upper_row_id: &str,
+    row_changeset: &mut RowChangeset,
+    cell_data: &SelectOptionCellDataPB,
+    to_row_id: &str,
 ) {
     cell_data.select_options.iter().for_each(|option| {
         if option.id == group.id && group.contains_row(&row_rev.id) {
-            changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+            group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
             group.remove_row(&row_rev.id);
         }
 
-        if let Some(index) = group.index_of_row(upper_row_id) {
+        if let Some(index) = group.index_of_row(to_row_id) {
             let row_pb = RowPB::from(row_rev);
             let inserted_row = InsertedRowPB {
                 row: row_pb.clone(),
                 index: Some(index as i32),
             };
-            changesets.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row]));
+            group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row]));
             group.insert_row(index, row_pb);
+
+            let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
+            row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
         }
     });
 }

+ 3 - 2
frontend/rust-lib/flowy-grid/src/services/group/group_service.rs

@@ -7,7 +7,7 @@ use crate::services::group::{
 };
 use bytes::Bytes;
 use flowy_error::FlowyResult;
-use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision};
+use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision, RowChangeset};
 use lib_infra::future::AFFuture;
 use std::future::Future;
 use std::sync::Arc;
@@ -96,6 +96,7 @@ impl GroupService {
     pub(crate) async fn did_move_row<F, O>(
         &self,
         row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
         upper_row_id: &str,
         get_field_fn: F,
     ) -> Option<Vec<GroupRowsChangesetPB>>
@@ -110,7 +111,7 @@ impl GroupService {
         match group_controller
             .write()
             .await
-            .did_move_row(row_rev, &field_rev, upper_row_id)
+            .did_move_row(row_rev, row_changeset, &field_rev, upper_row_id)
         {
             Ok(changesets) => Some(changesets),
             Err(e) => {

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs

@@ -3,7 +3,7 @@ use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest};
 use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER};
 use flowy_grid::entities::FieldType;
 use flowy_grid::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK};
-use flowy_grid_data_model::revision::RowMetaChangeset;
+use flowy_grid_data_model::revision::RowChangeset;
 
 #[tokio::test]
 async fn grid_create_row_count_test() {
@@ -24,7 +24,7 @@ async fn grid_create_row_count_test() {
 async fn grid_update_row() {
     let mut test = GridRowTest::new().await;
     let row_rev = test.row_builder().build();
-    let changeset = RowMetaChangeset {
+    let changeset = RowChangeset {
         row_id: row_rev.id.clone(),
         height: None,
         visibility: None,

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs

@@ -5,7 +5,7 @@ use crate::grid::grid_editor::GridEditorTest;
 use flowy_grid::entities::{CreateRowParams, FieldType, GridCellIdParams, GridLayout, RowPB};
 use flowy_grid::services::field::*;
 use flowy_grid_data_model::revision::{
-    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
+    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowChangeset, RowRevision,
 };
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -17,7 +17,7 @@ pub enum RowScript {
         row_rev: RowRevision,
     },
     UpdateRow {
-        changeset: RowMetaChangeset,
+        changeset: RowChangeset,
     },
     AssertRow {
         expected_row: RowRevision,

+ 16 - 1
shared-lib/flowy-grid-data-model/src/revision/grid_block.rs

@@ -42,13 +42,28 @@ impl RowRevision {
     }
 }
 #[derive(Debug, Clone, Default)]
-pub struct RowMetaChangeset {
+pub struct RowChangeset {
     pub row_id: String,
     pub height: Option<i32>,
     pub visibility: Option<bool>,
     pub cell_by_field_id: HashMap<FieldId, CellRevision>,
 }
 
+impl RowChangeset {
+    pub fn new(row_id: String) -> Self {
+        Self {
+            row_id,
+            height: None,
+            visibility: None,
+            cell_by_field_id: Default::default()
+        }
+    }
+
+    pub fn has_changed(&self) -> bool {
+        self.height.is_some() || self.visibility.is_some() || !self.cell_by_field_id.is_empty()
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
 pub struct CellRevision {
     pub data: String,

+ 2 - 2
shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs

@@ -2,7 +2,7 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision};
 use crate::errors::{CollaborateError, CollaborateResult};
 use crate::util::{cal_diff, make_text_delta_from_revisions};
 use flowy_grid_data_model::revision::{
-    gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision,
+    gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowChangeset, RowRevision,
 };
 use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder};
 use std::borrow::Cow;
@@ -143,7 +143,7 @@ impl GridBlockRevisionPad {
         self.block.rows.iter().position(|row| row.id == row_id)
     }
 
-    pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult<Option<GridBlockRevisionChangeset>> {
+    pub fn update_row(&mut self, changeset: RowChangeset) -> CollaborateResult<Option<GridBlockRevisionChangeset>> {
         let row_id = changeset.row_id.clone();
         self.modify_row(&row_id, |row| {
             let mut is_changed = None;