Browse Source

feat: support group by url in kanban board (#1687)

* feat: WIP on url controller

* fix: logging correct field

* chore: generate groups

* chore: revert change on URLTypeOptionPB

* chore: add tests + fix move row in group by url

* chore: rename test function

Co-authored-by: nathan <[email protected]>
Mohammad Zolfaghari 2 years ago
parent
commit
5d125091d9

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

@@ -737,6 +737,7 @@ class FieldInfo {
 
 
   bool get canBeGroup {
   bool get canBeGroup {
     switch (_field.fieldType) {
     switch (_field.fieldType) {
+      case FieldType.URL:
       case FieldType.Checkbox:
       case FieldType.Checkbox:
       case FieldType.MultiSelect:
       case FieldType.MultiSelect:
       case FieldType.SingleSelect:
       case FieldType.SingleSelect:

+ 42 - 11
frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs

@@ -497,6 +497,17 @@ impl BoxCellData {
         }
         }
     }
     }
 
 
+    fn unbox_or_none<T>(self) -> Option<T>
+    where
+        T: Default + 'static,
+    {
+        match self.0.downcast::<T>() {
+            Ok(value) => Some(*value),
+            Err(_) => None,
+        }
+    }
+
+    #[allow(dead_code)]
     fn downcast_ref<T: 'static>(&self) -> Option<&T> {
     fn downcast_ref<T: 'static>(&self) -> Option<&T> {
         self.0.downcast_ref()
         self.0.downcast_ref()
     }
     }
@@ -509,16 +520,36 @@ pub struct RowSingleCellData {
     pub cell_data: BoxCellData,
     pub cell_data: BoxCellData,
 }
 }
 
 
-impl RowSingleCellData {
-    pub fn get_text_field_cell_data(&self) -> Option<&<RichTextTypeOptionPB as TypeOption>::CellData> {
-        self.cell_data.downcast_ref()
-    }
-
-    pub fn get_number_field_cell_data(&self) -> Option<&<NumberTypeOptionPB as TypeOption>::CellData> {
-        self.cell_data.downcast_ref()
-    }
+macro_rules! into_cell_data {
+    ($func_name:ident,$return_ty:ty) => {
+        #[allow(dead_code)]
+        pub fn $func_name(self) -> Option<$return_ty> {
+            self.cell_data.unbox_or_none()
+        }
+    };
+}
 
 
-    pub fn get_url_field_cell_data(&self) -> Option<&<URLTypeOptionPB as TypeOption>::CellData> {
-        self.cell_data.downcast_ref()
-    }
+impl RowSingleCellData {
+    into_cell_data!(
+        into_text_field_cell_data,
+        <RichTextTypeOptionPB as TypeOption>::CellData
+    );
+    into_cell_data!(
+        into_number_field_cell_data,
+        <NumberTypeOptionPB as TypeOption>::CellData
+    );
+    into_cell_data!(into_url_field_cell_data, <URLTypeOptionPB as TypeOption>::CellData);
+    into_cell_data!(
+        into_single_select_field_cell_data,
+        <SingleSelectTypeOptionPB as TypeOption>::CellData
+    );
+    into_cell_data!(
+        into_multi_select_field_cell_data,
+        <MultiSelectTypeOptionPB as TypeOption>::CellData
+    );
+    into_cell_data!(into_date_field_cell_data, <DateTypeOptionPB as TypeOption>::CellData);
+    into_cell_data!(
+        into_check_list_field_cell_data,
+        <CheckboxTypeOptionPB as TypeOption>::CellData
+    );
 }
 }

+ 4 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs

@@ -32,7 +32,10 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
 #[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
 #[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
 pub struct URLTypeOptionPB {
 pub struct URLTypeOptionPB {
     #[pb(index = 1)]
     #[pb(index = 1)]
-    data: String, //It's not used yet.
+    pub url: String,
+
+    #[pb(index = 2)]
+    pub content: String,
 }
 }
 impl_type_option!(URLTypeOptionPB, FieldType::URL);
 impl_type_option!(URLTypeOptionPB, FieldType::URL);
 
 

+ 7 - 0
frontend/rust-lib/flowy-grid/src/services/group/configuration.rs

@@ -321,6 +321,13 @@ where
         Ok(())
         Ok(())
     }
     }
 
 
+    pub(crate) async fn get_all_cells(&self) -> Vec<RowSingleCellData> {
+        self.reader
+            .get_configuration_cells(&self.field_rev.id)
+            .await
+            .unwrap_or_default()
+    }
+
     fn mut_configuration(
     fn mut_configuration(
         &mut self,
         &mut self,
         mut_configuration_fn: impl FnOnce(&mut GroupConfigurationRevision) -> bool,
         mut_configuration_fn: impl FnOnce(&mut GroupConfigurationRevision) -> bool,

+ 2 - 0
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/mod.rs

@@ -1,7 +1,9 @@
 mod checkbox_controller;
 mod checkbox_controller;
 mod default_controller;
 mod default_controller;
 mod select_option_controller;
 mod select_option_controller;
+mod url_controller;
 
 
 pub use checkbox_controller::*;
 pub use checkbox_controller::*;
 pub use default_controller::*;
 pub use default_controller::*;
 pub use select_option_controller::*;
 pub use select_option_controller::*;
+pub use url_controller::*;

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

@@ -1,5 +1,5 @@
 use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB};
 use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB};
-use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell};
+use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell, insert_url_cell};
 use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB, CHECK};
 use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB, CHECK};
 use crate::services::group::configuration::GroupContext;
 use crate::services::group::configuration::GroupContext;
 use crate::services::group::controller::MoveGroupRowContext;
 use crate::services::group::controller::MoveGroupRowContext;
@@ -143,6 +143,10 @@ pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Opti
             let cell_rev = insert_checkbox_cell(group_id == CHECK, field_rev);
             let cell_rev = insert_checkbox_cell(group_id == CHECK, field_rev);
             Some(cell_rev)
             Some(cell_rev)
         }
         }
+        FieldType::URL => {
+            let cell_rev = insert_url_cell(group_id.to_owned(), field_rev);
+            Some(cell_rev)
+        }
         _ => {
         _ => {
             tracing::warn!("Unknown field type: {:?}", field_type);
             tracing::warn!("Unknown field type: {:?}", field_type);
             None
             None

+ 136 - 0
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/url_controller.rs

@@ -0,0 +1,136 @@
+use crate::entities::{GroupRowsNotificationPB, InsertedRowPB, RowPB};
+use crate::services::cell::insert_url_cell;
+use crate::services::field::{URLCellDataPB, URLCellDataParser, URLTypeOptionPB};
+use crate::services::group::action::GroupControllerCustomActions;
+use crate::services::group::configuration::GroupContext;
+use crate::services::group::controller::{
+    GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
+};
+use crate::services::group::{make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroupContext};
+use grid_rev_model::{CellRevision, FieldRevision, GroupRevision, RowRevision, URLGroupConfigurationRevision};
+
+pub type URLGroupController =
+    GenericGroupController<URLGroupConfigurationRevision, URLTypeOptionPB, URLGroupGenerator, URLCellDataParser>;
+
+pub type URLGroupContext = GroupContext<URLGroupConfigurationRevision>;
+
+impl GroupControllerCustomActions for URLGroupController {
+    type CellDataType = URLCellDataPB;
+
+    fn default_cell_rev(&self) -> Option<CellRevision> {
+        Some(CellRevision::new("".to_string()))
+    }
+
+    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool {
+        cell_data.content == content
+    }
+
+    fn add_or_remove_row_in_groups_if_match(
+        &mut self,
+        row_rev: &RowRevision,
+        cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsNotificationPB> {
+        let mut changesets = vec![];
+        self.group_ctx.iter_mut_status_groups(|group| {
+            let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
+            if group.id == cell_data.content {
+                if !group.contains_row(&row_rev.id) {
+                    let row_pb = RowPB::from(row_rev);
+                    changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone()));
+                    group.add_row(row_pb);
+                }
+            } else if group.contains_row(&row_rev.id) {
+                changeset.deleted_rows.push(row_rev.id.clone());
+                group.remove_row(&row_rev.id);
+            }
+
+            if !changeset.is_empty() {
+                changesets.push(changeset);
+            }
+        });
+        changesets
+    }
+
+    fn delete_row(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB> {
+        let mut changesets = vec![];
+        self.group_ctx.iter_mut_groups(|group| {
+            let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
+            if group.contains_row(&row_rev.id) {
+                changeset.deleted_rows.push(row_rev.id.clone());
+                group.remove_row(&row_rev.id);
+            }
+
+            if !changeset.is_empty() {
+                changesets.push(changeset);
+            }
+        });
+        changesets
+    }
+
+    fn move_row(
+        &mut self,
+        _cell_data: &Self::CellDataType,
+        mut context: MoveGroupRowContext,
+    ) -> Vec<GroupRowsNotificationPB> {
+        let mut group_changeset = vec![];
+        self.group_ctx.iter_mut_groups(|group| {
+            if let Some(changeset) = move_group_row(group, &mut context) {
+                group_changeset.push(changeset);
+            }
+        });
+        group_changeset
+    }
+}
+
+impl GroupController for URLGroupController {
+    fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
+        match self.group_ctx.get_group(group_id) {
+            None => tracing::warn!("Can not find the group: {}", group_id),
+            Some((_, group)) => {
+                let cell_rev = insert_url_cell(group.id.clone(), field_rev);
+                row_rev.cells.insert(field_rev.id.clone(), cell_rev);
+            }
+        }
+    }
+
+    fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) {
+        if let Some(group) = self.group_ctx.get_mut_group(group_id) {
+            group.add_row(row_pb.clone())
+        }
+    }
+}
+
+pub struct URLGroupGenerator();
+impl GroupGenerator for URLGroupGenerator {
+    type Context = URLGroupContext;
+    type TypeOptionType = URLTypeOptionPB;
+
+    fn generate_groups(
+        field_rev: &FieldRevision,
+        group_ctx: &Self::Context,
+        _type_option: &Option<Self::TypeOptionType>,
+    ) -> GeneratedGroupContext {
+        // Read all the cells for the grouping field
+        let cells = futures::executor::block_on(group_ctx.get_all_cells());
+
+        // Generate the groups
+        let group_configs = cells
+            .into_iter()
+            .flat_map(|value| value.into_url_field_cell_data())
+            .map(|cell| {
+                let group_id = cell.content.clone();
+                let group_name = cell.content.clone();
+                GeneratedGroupConfig {
+                    group_rev: GroupRevision::new(group_id, group_name),
+                    filter_content: cell.content.clone(),
+                }
+            })
+            .collect();
+
+        let no_status_group = Some(make_no_status_group(field_rev));
+        GeneratedGroupContext {
+            no_status_group,
+            group_configs,
+        }
+    }
+}

+ 10 - 3
frontend/rust-lib/flowy-grid/src/services/group/group_util.rs

@@ -3,13 +3,14 @@ use crate::services::group::configuration::GroupConfigurationReader;
 use crate::services::group::controller::GroupController;
 use crate::services::group::controller::GroupController;
 use crate::services::group::{
 use crate::services::group::{
     CheckboxGroupContext, CheckboxGroupController, DefaultGroupController, GroupConfigurationWriter,
     CheckboxGroupContext, CheckboxGroupController, DefaultGroupController, GroupConfigurationWriter,
-    MultiSelectGroupController, SelectOptionGroupContext, SingleSelectGroupController,
+    MultiSelectGroupController, SelectOptionGroupContext, SingleSelectGroupController, URLGroupContext,
+    URLGroupController,
 };
 };
 use flowy_error::FlowyResult;
 use flowy_error::FlowyResult;
 use grid_rev_model::{
 use grid_rev_model::{
     CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
     CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
     GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision,
     GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision,
-    SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
+    SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, URLGroupConfigurationRevision,
 };
 };
 use std::sync::Arc;
 use std::sync::Arc;
 
 
@@ -64,6 +65,12 @@ where
             let controller = CheckboxGroupController::new(&field_rev, configuration).await?;
             let controller = CheckboxGroupController::new(&field_rev, configuration).await?;
             group_controller = Box::new(controller);
             group_controller = Box::new(controller);
         }
         }
+        FieldType::URL => {
+            let configuration =
+                URLGroupContext::new(view_id, field_rev.clone(), configuration_reader, configuration_writer).await?;
+            let controller = URLGroupController::new(&field_rev, configuration).await?;
+            group_controller = Box::new(controller);
+        }
         _ => {
         _ => {
             group_controller = Box::new(DefaultGroupController::new(&field_rev));
             group_controller = Box::new(DefaultGroupController::new(&field_rev));
         }
         }
@@ -131,7 +138,7 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
                 .unwrap()
                 .unwrap()
         }
         }
         FieldType::URL => {
         FieldType::URL => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap()
+            GroupConfigurationRevision::new(field_id, field_type_rev, URLGroupConfigurationRevision::default()).unwrap()
         }
         }
     }
     }
 }
 }

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

@@ -68,8 +68,8 @@ async fn text_cell_date_test() {
         .await
         .await
         .unwrap();
         .unwrap();
 
 
-    for (i, cell) in cells.iter().enumerate() {
-        let text = cell.get_text_field_cell_data().unwrap();
+    for (i, cell) in cells.into_iter().enumerate() {
+        let text = cell.into_text_field_cell_data().unwrap();
         match i {
         match i {
             0 => assert_eq!(text.as_str(), "A"),
             0 => assert_eq!(text.as_str(), "A"),
             1 => assert_eq!(text.as_str(), ""),
             1 => assert_eq!(text.as_str(), ""),
@@ -92,10 +92,11 @@ async fn url_cell_date_test() {
         .await
         .await
         .unwrap();
         .unwrap();
 
 
-    for (i, cell) in cells.iter().enumerate() {
-        let url_cell_data = cell.get_url_field_cell_data().unwrap();
-        if i == 0 {
-            assert_eq!(url_cell_data.url.as_str(), "https://www.appflowy.io/")
+    for (i, cell) in cells.into_iter().enumerate() {
+        let url_cell_data = cell.into_url_field_cell_data().unwrap();
+        match i {
+            0 => assert_eq!(url_cell_data.url.as_str(), "https://www.appflowy.io/"),
+            _ => {}
         }
         }
     }
     }
 }
 }

+ 3 - 0
frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs

@@ -487,6 +487,7 @@ fn make_test_board() -> BuildGridContext {
                         FieldType::MultiSelect => row_builder
                         FieldType::MultiSelect => row_builder
                             .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
                             .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
                         _ => "".to_owned(),
                         _ => "".to_owned(),
                     };
                     };
                 }
                 }
@@ -522,6 +523,7 @@ fn make_test_board() -> BuildGridContext {
                             row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)])
                             row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)])
                         }
                         }
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        FieldType::URL => row_builder.insert_url_cell("https://github.com/AppFlowy-IO/AppFlowy"),
                         _ => "".to_owned(),
                         _ => "".to_owned(),
                     };
                     };
                 }
                 }
@@ -536,6 +538,7 @@ fn make_test_board() -> BuildGridContext {
                             row_builder.insert_single_select_cell(|mut options| options.remove(1))
                             row_builder.insert_single_select_cell(|mut options| options.remove(1))
                         }
                         }
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
                         _ => "".to_owned(),
                         _ => "".to_owned(),
                     };
                     };
                 }
                 }

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

@@ -244,6 +244,18 @@ impl GridGroupTest {
             .await
             .await
             .unwrap();
             .unwrap();
     }
     }
+
+    pub async fn get_url_field(&self) -> Arc<FieldRevision> {
+        self.inner
+            .field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type.is_url()
+            })
+            .unwrap()
+            .clone()
+    }
 }
 }
 
 
 impl std::ops::Deref for GridGroupTest {
 impl std::ops::Deref for GridGroupTest {

+ 49 - 0
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -486,3 +486,52 @@ async fn group_group_by_other_field() {
     ];
     ];
     test.run_scripts(scripts).await;
     test.run_scripts(scripts).await;
 }
 }
+
+#[tokio::test]
+async fn group_group_by_url() {
+    let mut test = GridGroupTest::new().await;
+    let url_field = test.get_url_field().await;
+    let scripts = vec![
+        GroupByField {
+            field_id: url_field.id.clone(),
+        },
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 2,
+        },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+        AssertGroupCount(3),
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 1,
+            to_row_index: 0,
+        },
+        MoveRow {
+            from_group_index: 1,
+            from_row_index: 0,
+            to_group_index: 2,
+            to_row_index: 0,
+        },
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 1,
+        },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 2,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}

+ 10 - 2
shared-lib/grid-rev-model/src/group_rev.rs

@@ -63,11 +63,11 @@ impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision {
 }
 }
 
 
 #[derive(Default, Serialize, Deserialize)]
 #[derive(Default, Serialize, Deserialize)]
-pub struct UrlGroupConfigurationRevision {
+pub struct URLGroupConfigurationRevision {
     pub hide_empty: bool,
     pub hide_empty: bool,
 }
 }
 
 
-impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision {
+impl GroupConfigurationContentSerde for URLGroupConfigurationRevision {
     fn from_json(s: &str) -> Result<Self, Error> {
     fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
         serde_json::from_str(s)
     }
     }
@@ -120,6 +120,14 @@ pub struct GroupRevision {
 const GROUP_REV_VISIBILITY: fn() -> bool = || true;
 const GROUP_REV_VISIBILITY: fn() -> bool = || true;
 
 
 impl GroupRevision {
 impl GroupRevision {
+    /// Create a new GroupRevision
+    ///
+    /// # Arguments
+    ///
+    /// * `id`: identifier for this group revision. This id must be unique.
+    /// * `group_name`: the name of this group
+    ///
+    /// returns: GroupRevision
     pub fn new(id: String, group_name: String) -> Self {
     pub fn new(id: String, group_name: String) -> Self {
         Self {
         Self {
             id,
             id,