feat: delete kanban board groups (#3925)

* feat: hide/unhide ui

* chore: implement collapsible side bar and adjust group header (#2)

* refactor: hidden columns into own file

* chore: adjust new group button position

* fix: flowy icon buton secondary color bleed

* chore: some UI adjustments

* fix: some regressions

* chore: proper group is_visible fetching

* chore: use a bloc to manage hidden groups

* fix: hiding groups not working

* chore: implement hidden group popups

* chore: proper ungrouped item column management

* chore: remove ungrouped items button

* chore: flowy hover build

* fix: clean up code

* test: integration tests

* fix: not null promise on null value

* fix: hide and unhide multiple groups

* chore: i18n and code review

* chore: missed review

* fix: rust-lib-test

* fix: dont completely remove flowyiconhovercolor

* chore: apply suggest

* fix: number of rows inside hidden groups not updating properly

* fix: hidden groups disappearing after collapse

* fix: hidden group title alignment

* fix: insert newly unhidden groups into the correct position

* chore: adjust padding all around

* feat: reorder hidden groups

* chore: adjust padding

* chore: collapse hidden groups section persist

* chore: no status group at beginning

* fix: hiding groups when grouping with other types

* chore: disable rename groups that arent supported

* chore: update appflowy board ref

* chore: better naming

* feat: delete kanban groups

* chore: forgot to save

* chore: fix build and small ui adjustments

* chore: add a confirm dialog when deleting a column

* fix: flutter lint

* test: add integration test

* chore: fix some design review issues

* chore: apply suggestions from Nathan

* fix: write lock on group controller

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
This commit is contained in:
Richard Shiue
2023-11-28 10:43:22 +08:00
committed by GitHub
parent 3595de5e12
commit 9d61ca0278
28 changed files with 476 additions and 187 deletions

View File

@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{FieldType, RowMetaPB};
use crate::entities::RowMetaPB;
use crate::services::group::{GroupChangeset, GroupData, GroupSetting};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@ -130,13 +130,6 @@ pub struct GroupByFieldParams {
pub view_id: String,
}
pub struct DeleteGroupParams {
pub view_id: String,
pub field_id: String,
pub group_id: String,
pub field_type: FieldType,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct UpdateGroupPB {
#[pb(index = 1)]
@ -230,3 +223,32 @@ impl TryFrom<CreateGroupPayloadPB> for CreateGroupParams {
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct DeleteGroupPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub group_id: String,
}
pub struct DeleteGroupParams {
pub view_id: String,
pub group_id: String,
}
impl TryFrom<DeleteGroupPayloadPB> for DeleteGroupParams {
type Error = ErrorCode;
fn try_from(value: DeleteGroupPayloadPB) -> Result<Self, Self::Error> {
let view_id = NotEmptyStr::parse(value.view_id)
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
.0;
let group_id = NotEmptyStr::parse(value.group_id)
.map_err(|_| ErrorCode::GroupIdIsEmpty)?
.0;
Ok(Self { view_id, group_id })
}
}

View File

@ -57,6 +57,10 @@ impl RowsChangePB {
..Default::default()
}
}
pub fn is_empty(&self) -> bool {
self.deleted_rows.is_empty() && self.inserted_rows.is_empty() && self.updated_rows.is_empty()
}
}
#[derive(Debug, Default, ProtoBuf)]

View File

@ -755,6 +755,18 @@ pub(crate) async fn create_group_handler(
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn delete_group_handler(
data: AFPluginData<DeleteGroupPayloadPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> FlowyResult<()> {
let manager = upgrade_manager(manager)?;
let params: DeleteGroupParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
database_editor.delete_group(params).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(manager), err)]
pub(crate) async fn get_databases_handler(
manager: AFPluginState<Weak<DatabaseManager>>,

View File

@ -61,6 +61,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::GetGroup, get_group_handler)
.event(DatabaseEvent::UpdateGroup, update_group_handler)
.event(DatabaseEvent::CreateGroup, create_group_handler)
.event(DatabaseEvent::DeleteGroup, delete_group_handler)
// Database
.event(DatabaseEvent::GetDatabases, get_databases_handler)
// Calendar
@ -288,6 +289,9 @@ pub enum DatabaseEvent {
#[event(input = "CreateGroupPayloadPB")]
CreateGroup = 114,
#[event(input = "DeleteGroupPayloadPB")]
DeleteGroup = 115,
/// Returns all the databases
#[event(output = "RepeatedDatabaseDescriptionPB")]
GetDatabases = 120,

View File

@ -174,12 +174,16 @@ impl DatabaseEditor {
}
pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self
.database
.lock()
.delete_group_setting(&params.view_id, &params.group_id);
let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_delete_group(params).await?;
let changes = view_editor.v_delete_group(&params.group_id).await?;
if !changes.is_empty() {
for view in self.database_views.editors().await {
send_notification(&view.view_id, DatabaseNotification::DidUpdateViewRows)
.payload(changes.clone())
.send();
}
}
Ok(())
}
@ -819,7 +823,7 @@ impl DatabaseEditor {
};
for option in options {
type_option.delete_option(option.into());
type_option.delete_option(&option.id);
}
self
.database
@ -1317,6 +1321,10 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
})
}
fn remove_row(&self, row_id: &RowId) -> Option<Row> {
self.database.lock().remove_row(row_id)
}
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {
let cells = self.database.lock().get_cells_for_field(view_id, field_id);
to_fut(async move { cells.into_iter().map(Arc::new).collect() })

View File

@ -14,8 +14,8 @@ use lib_dispatch::prelude::af_spawn;
use crate::entities::{
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams,
DeleteGroupParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB,
InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB,
DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB,
LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB,
SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams,
};
use crate::notification::{send_notification, DatabaseNotification};
@ -391,8 +391,43 @@ impl DatabaseViewEditor {
Ok(())
}
pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> {
Ok(())
pub async fn v_delete_group(&self, group_id: &str) -> FlowyResult<RowsChangePB> {
let mut group_controller = self.group_controller.write().await;
let controller = match group_controller.as_mut() {
Some(controller) => controller,
None => return Ok(RowsChangePB::default()),
};
let old_field = self.delegate.get_field(controller.field_id());
let (row_ids, type_option_data) = controller.delete_group(group_id)?;
drop(group_controller);
let mut changes = RowsChangePB::default();
if let Some(field) = old_field {
let deleted_rows = row_ids
.iter()
.filter_map(|row_id| self.delegate.remove_row(row_id))
.map(|row| row.id.into_inner());
changes.deleted_rows.extend(deleted_rows);
if let Some(type_option) = type_option_data {
self
.delegate
.update_field(&self.view_id, type_option, field)
.await?;
}
let notification = GroupChangesPB {
view_id: self.view_id.clone(),
deleted_groups: vec![group_id.to_string()],
..Default::default()
};
notify_did_update_num_of_groups(&self.view_id, notification).await;
}
Ok(changes)
}
pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> {

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{RowCell, RowDetail, RowId};
use collab_database::rows::{Row, RowCell, RowDetail, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
use tokio::sync::RwLock;
@ -57,6 +57,8 @@ pub trait DatabaseViewOperation: Send + Sync + 'static {
/// Returns all the rows in the view
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>;
fn remove_row(&self, row_id: &RowId) -> Option<Row>;
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>;
fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>>;

View File

@ -49,12 +49,9 @@ pub trait SelectTypeOptionSharedAction: Send + Sync {
}
}
fn delete_option(&mut self, delete_option: SelectOption) {
fn delete_option(&mut self, option_id: &str) {
let options = self.mut_options();
if let Some(index) = options
.iter()
.position(|option| option.id == delete_option.id)
{
if let Some(index) = options.iter().position(|option| option.id == option_id) {
options.remove(index);
}
}

View File

@ -1,6 +1,6 @@
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Row, RowDetail};
use collab_database::rows::{Cell, Row, RowDetail, RowId};
use flowy_error::FlowyResult;
@ -78,6 +78,8 @@ pub trait GroupCustomize: Send + Sync {
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
Ok((None, None))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>;
}
/// Defines the shared actions any group controller can perform.
@ -159,6 +161,14 @@ pub trait GroupControllerOperation: Send + Sync {
/// * `field`: new changeset
fn did_update_group_field(&mut self, field: &Field) -> FlowyResult<Option<GroupChangesPB>>;
/// Delete a group from the group configuration.
///
/// Return a list of deleted row ids and/or a new `TypeOptionData` if
/// successful.
///
/// * `group_id`: the id of the group to be deleted
fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)>;
/// Updates the name and/or visibility of groups.
///
/// Returns a non-empty `TypeOptionData` when the changes require a change

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail};
use collab_database::rows::{Cells, Row, RowDetail, RowId};
use futures::executor::block_on;
use serde::de::DeserializeOwned;
use serde::Serialize;
@ -396,6 +396,27 @@ where
Ok(None)
}
fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> {
let group = if group_id != self.field_id() {
self.get_group(group_id)
} else {
None
};
match group {
Some((_index, group_data)) => {
let row_ids = group_data
.rows
.iter()
.map(|row| row.row.id.clone())
.collect();
let type_option_data = self.delete_group_custom(group_id)?;
Ok((row_ids, type_option_data))
},
None => Ok((vec![], None)),
}
}
async fn apply_group_changeset(
&mut self,
changeset: &GroupChangesets,

View File

@ -1,6 +1,7 @@
use async_trait::async_trait;
use collab_database::fields::Field;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB};
@ -138,6 +139,10 @@ impl GroupCustomize for CheckboxGroupController {
});
group_changeset
}
fn delete_group_custom(&mut self, _group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
Ok(None)
}
}
impl GroupController for CheckboxGroupController {

View File

@ -7,7 +7,7 @@ use chrono::{
};
use chrono_tz::Tz;
use collab_database::database::timestamp;
use collab_database::fields::Field;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@ -248,6 +248,11 @@ impl GroupCustomize for DateGroupController {
}
deleted_group
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
self.context.delete_group(group_id)?;
Ok(None)
}
}
impl GroupController for DateGroupController {

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail};
use collab_database::rows::{Cells, Row, RowDetail, RowId};
use flowy_error::FlowyResult;
@ -129,6 +129,10 @@ impl GroupControllerOperation for DefaultGroupController {
Ok(None)
}
fn delete_group(&mut self, _group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> {
Ok((vec![], None))
}
async fn apply_group_changeset(
&mut self,
_changeset: &GroupChangesets,

View File

@ -107,6 +107,22 @@ impl GroupCustomize for MultiSelectGroupController {
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
if let Some(option_index) = self
.type_option
.options
.iter()
.position(|option| option.id == group_id)
{
// Remove the option if the group is found
let mut new_type_option = self.type_option.clone();
new_type_option.options.remove(option_index);
Ok(Some(new_type_option.into()))
} else {
Ok(None)
}
}
}
impl GroupController for MultiSelectGroupController {

View File

@ -111,6 +111,23 @@ impl GroupCustomize for SingleSelectGroupController {
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
if let Some(option_index) = self
.type_option
.options
.iter()
.position(|option| option.id == group_id)
{
// Remove the option if the group is found
let mut new_type_option = self.type_option.clone();
new_type_option.options.remove(option_index);
Ok(Some(new_type_option.into()))
} else {
// Return None if no matching group is found
Ok(None)
}
}
}
impl GroupController for SingleSelectGroupController {

View File

@ -1,7 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::Field;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use serde::{Deserialize, Serialize};
@ -186,6 +186,11 @@ impl GroupCustomize for URLGroupController {
}
deleted_group
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
self.context.delete_group(group_id)?;
Ok(None)
}
}
impl GroupController for URLGroupController {