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 <nathan@appflowy.io>
This commit is contained in:
Mohammad Zolfaghari 2023-01-19 13:26:55 +03:30 committed by GitHub
parent 9c44b30847
commit 5d125091d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 289 additions and 25 deletions

View File

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

View File

@ -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> {
self.0.downcast_ref()
}
@ -509,16 +520,36 @@ pub struct RowSingleCellData {
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()
}
pub fn get_url_field_cell_data(&self) -> Option<&<URLTypeOptionPB 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()
}
};
}
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
);
}

View File

@ -32,7 +32,10 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct URLTypeOptionPB {
#[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);

View File

@ -321,6 +321,13 @@ where
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(
&mut self,
mut_configuration_fn: impl FnOnce(&mut GroupConfigurationRevision) -> bool,

View File

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

View File

@ -1,5 +1,5 @@
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::group::configuration::GroupContext;
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);
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);
None

View File

@ -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,
}
}
}

View File

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

View File

@ -68,8 +68,8 @@ async fn text_cell_date_test() {
.await
.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 {
0 => assert_eq!(text.as_str(), "A"),
1 => assert_eq!(text.as_str(), ""),
@ -92,10 +92,11 @@ async fn url_cell_date_test() {
.await
.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/"),
_ => {}
}
}
}

View File

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

View File

@ -244,6 +244,18 @@ impl GridGroupTest {
.await
.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 {

View File

@ -486,3 +486,52 @@ async fn group_group_by_other_field() {
];
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;
}

View File

@ -63,11 +63,11 @@ impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision {
}
#[derive(Default, Serialize, Deserialize)]
pub struct UrlGroupConfigurationRevision {
pub struct URLGroupConfigurationRevision {
pub hide_empty: bool,
}
impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision {
impl GroupConfigurationContentSerde for URLGroupConfigurationRevision {
fn from_json(s: &str) -> Result<Self, Error> {
serde_json::from_str(s)
}
@ -120,6 +120,14 @@ pub struct GroupRevision {
const GROUP_REV_VISIBILITY: fn() -> bool = || true;
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 {
Self {
id,