diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index eca01d1bed..1fd48702ac 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -221,5 +221,10 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "board": { + "column": { + "create_new_card": "New" + } } -} +} \ No newline at end of file diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index f6bfd609c3..e850186f89 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -2,6 +2,7 @@ import 'dart:collection'; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; @@ -9,12 +10,14 @@ import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart' import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group.pbserver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../grid/application/row/row_cache.dart'; @@ -109,7 +112,7 @@ class _BoardContentState extends State { column, columnItem, ), - columnConstraints: const BoxConstraints.tightFor(width: 240), + columnConstraints: const BoxConstraints.tightFor(width: 300), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ), @@ -154,7 +157,11 @@ class _BoardContentState extends State { } Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { - return AppFlowyColumnFooter( + final group = columnData.customData as GroupPB; + if (group.isDefault) { + return const SizedBox(); + } else { + return AppFlowyColumnFooter( icon: SizedBox( height: 20, width: 20, @@ -164,7 +171,7 @@ class _BoardContentState extends State { ), ), title: FlowyText.medium( - "New", + LocaleKeys.board_column_create_new_card.tr(), fontSize: 14, color: context.read().textColor, ), @@ -172,7 +179,9 @@ class _BoardContentState extends State { margin: config.footerPadding, onAddButtonClick: () { context.read().add(BoardEvent.createRow(columnData.id)); - }); + }, + ); + } } Widget _buildCard( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 0e0a7287ae..3ca934e508 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -93,14 +93,15 @@ BoxDecoration _makeBoxDecoration(BuildContext context) { final theme = context.read(); final borderSide = BorderSide(color: theme.shader6, width: 1.0); return BoxDecoration( - color: theme.surface, + color: Colors.transparent, border: Border.fromBorderSide(borderSide), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: theme.shader6, - spreadRadius: 0, - blurRadius: 2, - offset: Offset.zero) + color: Colors.transparent, + spreadRadius: 0, + blurRadius: 2, + offset: Offset.zero, + ) ], borderRadius: const BorderRadius.all(Radius.circular(6)), ); @@ -120,8 +121,9 @@ class _CardEnterRegion extends StatelessWidget { builder: (context, onEnter, _) { List children = [child]; if (onEnter) { - children.add(CardAccessoryContainer(accessories: accessories) - .positioned(right: 0)); + children.add(CardAccessoryContainer( + accessories: accessories, + ).positioned(right: 0)); } return MouseRegion( diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index 9cc138bc0f..75b133344f 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -81,6 +81,9 @@ pub struct GroupPB { #[pb(index = 4)] pub rows: Vec, + + #[pb(index = 5)] + pub is_default: bool, } impl std::convert::From for GroupPB { @@ -90,6 +93,7 @@ impl std::convert::From for GroupPB { group_id: group.id, desc: group.name, rows: group.rows, + is_default: group.is_default, } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 294aff9885..ecff3b951b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -176,6 +176,12 @@ pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) - CellRevision::new(data) } +pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision { + let cell_data = SelectOptionCellChangeset::from_delete(&option_id).to_str(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); + CellRevision::new(data) +} + /// If the cell data is not String type, it should impl this trait. /// Deserialize the String into cell specific data type. pub trait FromCellString { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 8c9893e4ef..70e4cc29b1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -142,7 +142,7 @@ impl GridViewManager { .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) .await; - if row_changeset.is_empty() == false { + if !row_changeset.is_empty() { with_row_changeset(row_changeset).await; } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index 546afb8a32..fea71ebbbc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -31,6 +31,10 @@ impl std::fmt::Display for GenericGroupConfiguration { self.groups_map.iter().for_each(|(_, group)| { let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); }); + let _ = f.write_fmt(format_args!( + "Default group has {} rows \n", + self.default_group.rows.len() + )); Ok(()) } } @@ -41,6 +45,8 @@ pub struct GenericGroupConfiguration { configuration_content: PhantomData, field_rev: Arc, groups_map: IndexMap, + /// default_group is used to store the rows that don't belong to any groups. + default_group: Group, writer: Arc, } @@ -55,6 +61,15 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { + let default_group_id = format!("{}_default_group", view_id); + let default_group = Group { + id: default_group_id, + field_id: field_rev.id.clone(), + name: format!("No {}", field_rev.name), + is_default: true, + rows: vec![], + content: "".to_string(), + }; let configuration = match reader.get_group_configuration(field_rev.clone()).await { None => { let default_group_configuration = default_group_configuration(&field_rev); @@ -71,6 +86,7 @@ where view_id, field_rev, groups_map: IndexMap::new(), + default_group, writer, configuration, configuration_content: PhantomData, @@ -82,7 +98,9 @@ where } pub(crate) fn clone_groups(&self) -> Vec { - self.groups_map.values().cloned().collect() + let mut groups: Vec = self.groups_map.values().cloned().collect(); + groups.push(self.default_group.clone()); + groups } pub(crate) fn merge_groups(&mut self, groups: Vec) -> FlowyResult> { @@ -160,6 +178,10 @@ where self.groups_map.get_mut(group_id) } + pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { + &mut self.default_group + } + pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { let from_index = self.groups_map.get_index_of(from_id); let to_index = self.groups_map.get_index_of(to_id); @@ -263,7 +285,7 @@ fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupR } // Find out the new groups - let new_groups = group_map.into_values().collect::>(); + let new_groups = group_map.into_values(); for (index, group) in new_groups.into_iter().enumerate() { merge_result.add_insert_group(index, group); } @@ -291,7 +313,7 @@ impl MergeGroupResult { } fn add_group(&mut self, group: Group) { - self.groups.push(group.clone()); + self.groups.push(group); } fn add_insert_group(&mut self, index: usize, group: Group) { @@ -309,11 +331,10 @@ fn make_group_view_changeset( inserted_groups: Vec, updated_group: Vec, ) -> GroupViewChangesetPB { - let changeset = GroupViewChangesetPB { + GroupViewChangesetPB { view_id, inserted_groups, deleted_groups: vec![], update_groups: updated_group.into_iter().map(GroupPB::from).collect(), - }; - changeset + } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index fac034f6f5..562f21eb17 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -11,8 +11,6 @@ use flowy_grid_data_model::revision::{ use std::marker::PhantomData; use std::sync::Arc; -const DEFAULT_GROUP_ID: &str = "default_group"; - // Each kind of group must implement this trait to provide custom group // operations. For example, insert cell data to the row_rev when creating // a new row. @@ -72,8 +70,6 @@ pub struct GenericGroupController { pub field_id: String, pub type_option: Option, pub configuration: GenericGroupConfiguration, - /// default_group is used to store the rows that don't belong to any groups. - default_group: Group, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData

, } @@ -92,22 +88,82 @@ where let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); let _ = configuration.merge_groups(groups)?; - let default_group = Group::new( - DEFAULT_GROUP_ID.to_owned(), - field_rev.id.clone(), - format!("No {}", field_rev.name), - "".to_string(), - ); Ok(Self { field_id: field_rev.id.clone(), - default_group, type_option, configuration, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData, }) } + + // https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect + #[allow(clippy::needless_collect)] + fn update_default_group( + &mut self, + row_rev: &RowRevision, + other_group_changesets: &[GroupChangesetPB], + ) -> GroupChangesetPB { + let default_group = self.configuration.get_mut_default_group(); + + // [other_group_inserted_row] contains all the inserted rows except the default group. + let other_group_inserted_row = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .collect::>(); + + // Calculate the inserted_rows of the default_group + let default_group_inserted_row = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .filter(|row_id| { + // if the [other_group_inserted_row] contains the row_id of the row + // which means the row should not move to the default group. + !other_group_inserted_row + .iter() + .any(|inserted_row| &inserted_row.row.id == row_id) + }) + .collect::>(); + + let mut changeset = GroupChangesetPB::new(default_group.id.clone()); + if !default_group_inserted_row.is_empty() { + changeset.inserted_rows.push(InsertedRowPB::new(row_rev.into())); + default_group.add_row(row_rev.into()); + } + + // [other_group_delete_rows] contains all the deleted rows except the default group. + let other_group_delete_rows: Vec = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .collect(); + + let default_group_deleted_rows = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .filter(|inserted_row| { + // if the [other_group_delete_rows] contain the inserted_row, which means this row should move + // out from the default_group. + let inserted_row_id = &inserted_row.row.id; + !other_group_delete_rows.iter().any(|row_id| inserted_row_id == row_id) + }) + .collect::>(); + + let mut deleted_row_ids = vec![]; + for row in &default_group.rows { + if default_group_deleted_rows + .iter() + .any(|deleted_row| deleted_row.row.id == row.id) + { + deleted_row_ids.push(row.id.clone()); + } + } + default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id)); + changeset.deleted_rows.extend(deleted_row_ids); + changeset + } } impl GroupControllerSharedOperation for GenericGroupController @@ -124,11 +180,7 @@ where } fn groups(&self) -> Vec { - let mut groups = self.configuration.clone_groups(); - if self.default_group.is_empty() == false { - groups.insert(0, self.default_group.clone()); - } - groups + self.configuration.clone_groups() } fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -138,7 +190,6 @@ where #[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))] fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { - // let mut ungrouped_rows = vec![]; for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let mut grouped_rows: Vec = vec![]; @@ -154,8 +205,7 @@ where } if grouped_rows.is_empty() { - // ungrouped_rows.push(RowPB::from(row_rev)); - self.default_group.add_row(row_rev.into()); + self.configuration.get_mut_default_group().add_row(row_rev.into()); } else { for group_row in grouped_rows { if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) { @@ -164,30 +214,11 @@ where } } } else { - self.default_group.add_row(row_rev.into()); + self.configuration.get_mut_default_group().add_row(row_rev.into()); } } - // if !ungrouped_rows.is_empty() { - // let default_group_rev = GroupRevision::default_group(gen_grid_group_id(), format!("No {}", field_rev.name)); - // let default_group = Group::new( - // default_group_rev.id.clone(), - // field_rev.id.clone(), - // default_group_rev.name.clone(), - // "".to_owned(), - // ); - // } - - tracing::Span::current().record( - "group_result", - &format!( - "{}, default_group has {} rows", - self.configuration, - self.default_group.rows.len() - ) - .as_str(), - ); - + tracing::Span::current().record("group_result", &format!("{},", self.configuration,).as_str()); Ok(self.groups()) } @@ -203,7 +234,12 @@ where 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::

()?; - let changesets = self.add_row_if_match(row_rev, &cell_data); + let mut changesets = self.add_row_if_match(row_rev, &cell_data); + let default_group_changeset = self.update_default_group(row_rev, &changesets); + tracing::info!("default_group_changeset: {}", default_group_changeset); + if !default_group_changeset.is_empty() { + changesets.push(default_group_changeset); + } Ok(changesets) } else { Ok(vec![]) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index 349f7391a6..39f581d97d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -15,18 +15,25 @@ pub fn add_row( row_rev: &RowRevision, ) -> Option { let mut changeset = GroupChangesetPB::new(group.id.clone()); - cell_data.select_options.iter().for_each(|option| { - if option.id == group.id { - 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) { + if cell_data.select_options.is_empty() { + if group.contains_row(&row_rev.id) { changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } - }); + } else { + cell_data.select_options.iter().for_each(|option| { + if option.id == group.id { + 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() { None diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs index f4d0ee1652..9afe7f54e9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -5,6 +5,7 @@ pub struct Group { pub id: String, pub field_id: String, pub name: String, + pub is_default: bool, pub(crate) rows: Vec, /// [content] is used to determine which group the cell belongs to. @@ -16,6 +17,7 @@ impl Group { Self { id, field_id, + is_default: false, name, rows: vec![], content, diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs index 3a5ae9455b..6afeda6d4b 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::grid_editor::GridEditorTest; use flowy_grid::entities::{ CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, }; -use flowy_grid::services::cell::insert_select_option_cell; +use flowy_grid::services::cell::{delete_select_option_cell, insert_select_option_cell}; use flowy_grid_data_model::revision::RowChangeset; use std::time::Duration; use tokio::time::interval; @@ -128,11 +128,22 @@ impl GridGroupTest { let field_id = from_group.field_id; let field_rev = self.editor.get_field_rev(&field_id).await.unwrap(); let field_type: FieldType = field_rev.ty.into(); - let cell_rev = match field_type { - FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), - FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), - _ => { - panic!("Unsupported group field type"); + + let cell_rev = if to_group.is_default { + match field_type { + FieldType::SingleSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev), + FieldType::MultiSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev), + _ => { + panic!("Unsupported group field type"); + } + } + } else { + match field_type { + FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), + FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), + _ => { + panic!("Unsupported group field type"); + } } }; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index 4a4d45f952..a52a088f51 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -6,7 +6,7 @@ use flowy_grid::entities::FieldChangesetParams; async fn group_init_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ - AssertGroupCount(3), + AssertGroupCount(4), AssertGroupRowCount { group_index: 0, row_count: 2, @@ -19,6 +19,10 @@ async fn group_init_test() { group_index: 2, row_count: 1, }, + AssertGroupRowCount { + group_index: 3, + row_count: 0, + }, ]; test.run_scripts(scripts).await; } @@ -294,6 +298,55 @@ async fn group_reorder_group_test() { test.run_scripts(scripts).await; } +#[tokio::test] +async fn group_move_to_default_group_test() { + let mut test = GridGroupTest::new().await; + let scripts = vec![ + UpdateRow { + from_group_index: 0, + row_index: 0, + to_group_index: 3, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_from_default_group_test() { + let mut test = GridGroupTest::new().await; + let scripts = vec![UpdateRow { + from_group_index: 0, + row_index: 0, + to_group_index: 3, + }]; + test.run_scripts(scripts).await; + + let scripts = vec![ + UpdateRow { + from_group_index: 3, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; +} + #[tokio::test] async fn group_move_group_test() { let mut test = GridGroupTest::new().await;