mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
9c44b30847
commit
5d125091d9
@ -737,6 +737,7 @@ class FieldInfo {
|
||||
|
||||
bool get canBeGroup {
|
||||
switch (_field.fieldType) {
|
||||
case FieldType.URL:
|
||||
case FieldType.Checkbox:
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.SingleSelect:
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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/"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user